JavaScript函数式编程
JavaScript中的函数式编程
我开始javascript编程已经有很长一段时间了,我见证了这门语言的成长,从一个使用jquery验证表单的简单工具,到能够开发庞大的后台应用。自从ES6出现和增加了classes,我认为这只会引用编程方式的混乱,使javascript的编程向java靠近。
自从接触了函数式编程,我看到了它完成适应javascript的潜力,并且能够写出更加简洁的代码。
什么是函数式编程
在计算机科学中,函数式编程是一种编程范式——构建计算机程序结构和元素的一种风格——它把计算当做数学上的函数对待,避免状态的改变以及可变数据。它是一种声明式的编程范式,因为程序是用表达式或者声明而不是使用语句来完成的。在函数中,输出的值只依赖于它的参数,换句话说相同的输出总能得出相同的输出。这与命令式编程相反,在命令式编程中,除了函数参数,全局程序装填也可以影响函数最终的结果。消除副作用,即不依赖于函数输出的状态变化,这能使程序更容易理解,这是使用函数式编程开发程序的主要动机之一。
我们将要学习的概念
- Lazy evaluation
- Monoid
- Monad
- Functor
- Curry
- Lambda
- Recursion
- Closure
- Stateless
- Compose
- High order functions
- Pure functions
- First Class
- Side effects
相关阅读
在我的笔记后面有一些非常有趣的链接
- adit.io/posts/2013-…
- github.com/haskellcama… (Nice!)
- leanpub.com/fljs
- medium.com/@lunasunkai… (Nice!)
- medium.com/@_cyberglot…? (Nice!)
惰性求值
定义
惰性求值,或者说按需调用是一种求值策略,将表达式的求值延迟到需要它的值时才执行(非严格求值),这也能够避免重复求值。
代码示例
惰性求值是将表达式求值延迟到以后执行,这可以用thunks实现。
译者注:将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数
示例1
// 非惰性求值
var value = 1 + 1 // 立即计算得到2
// 惰性求值
var lazyValue = () => 1 + 1 // 当lazyValue被调用时得到值为2
示例2
// 非惰性求值
var add = (x, y) => x + y
var result = add(1, 2) // 立即计算得到值为3
// 惰性求值
var addLazy = (x, y) => () => x + y;
var result = addLazy(1, 2) // 返回一个`thunk`,当需要计算时得到结果为3
示例3
// 科里化相加(非惰性求值)
var add = x => y => x + y
var add3 = add(3)
var result = add3(7) // 立即计算得到值为10
示例4
// 非惰性求值
var callApi = spec => fetch(spec.url, spec.options);
var result = callApi({url: '/api', options: {}});
// 惰性求值
var callApiLazy = spec => () => fetch(spec.url, spec.options);
var result = callApiLazy({url: '/api', options: {}});
Monoid(幺半群)
定义
它描述了一组元素,当这些元素与特定的操作(通常称为concat)结合使用时,这些元素具有3个特殊属性:
译者注:幺半群是一个带有二元运算 *: M × M → M 的集合 M
这个操作符必须把集合中的两个值合并为同一集合中的第三个值。假设a和b都是集合的一部分,那么concat(a,b)都必须是这个集合中的一部分。在范畴论中,这被称作magma
译者注:即封闭性——对任何在 *M* 内的 *a* 、 *b* , *a***b* 也会在 *M* 内。
操作符必须满足结合律:concat(x, concat(y, z))必须等于concat(concat(x, y), z) ,这里的x,y,z可以使集合中的任意值。无论如何组织操作符,只要遵循规则,结果应该是相同的。
译者注:即结合律——对任何在 *M* 内的*a*、*b*、*c* , (*a***b*)**c* = *a**(*b***c*)
集合中应该有与操作符相关的中性元素,如果中性元素与其他任意值组合,这个值不应该被改变。 concat(element, neutral) == concat(neutral, element) == element
译者注:即单位元:存在一在 *M* 内的元素*e*,使得任一于 *M* 内的 *a* 都会符合 *a***e* = *e***a* = *a*
译者注:例如在实数这个集合中,0即是实数集合对应于加法的单位元,实数中的任何元素与0相加都不会改变元素的值
代码示例
Monoid 的例子,可以是字符串连接,数字相加,函数组合,异步组合。
示例1——Strings
const concat = (a, b) => a.concat(b);
concat("hello", concat(" ", "world")); // "hello world"
concat(concat("hello", " "), "world"); // "hello world"
concat("hello", ""); // 'hello'
concat("", "hello"); // 'hello'
示例2——Numbers
(1 + 2) + 3 == 1 + (2 + 3); // true
x + 0; // x
示例3——Functions
const compose = (func1, func2) => arg => func1(func2(arg));
const add5 = a => a + 5;
const double = a => a * 2;
const doubleThenAdd5 = compose(add5,double);
doubleThenAdd5(3); // 11
Functor(函子)
定义
函子就是可以进行映射到上面的东西。是任何可以使用映射的东西,在实践中,函子表示一种类型,可以映射...
代码示例
换句话说,就是任何对象我们都可以进行映射,并应用函数生成另一个相同类型和连接的对象实例。
示例1
[1, 2, 3].map(val => val * 2); //得到 [2, 4, 6], Array是一个函子
示例2
// 数字->字符串映射
const numberToString = num => num.toString()
示例3
const Identity = value => ({
map: fn => Identity(fn(value)),
});
const myFunctor = Identity(1);
myFunctor.map(trace); // 1
myFunctor.map(myFunction).map(trace); // 2
纯函数
定义
函数是一个过程,它接受一些输入,称为参数,并产生一些输出,称为返回值。
代码示例
有纯函数和非纯函数,如果函数修改外部返回值,就会产生副作用。
- 给定同样的输入,会产生同样的输出
- 不会产生副作用
示例1
//纯函数
const add = (x, y) => x+y;
const sub = (x, y) => x-y;
//非纯函数
let tmp = 2;
const concat = (a,b)=>{
tmp = 3;
return a+b;
};
示例2
const double = x => x * 2;
示例3
function add(a, b) {
return a + b;
}
function mul(a, b) {
return a * b;
}
let x = add(2, mul(3, 4));
副作用
定义
副作用是指除了返回值之外,在被调用函数之外可以观察到的任何应用状态变化。
- 修改任何外部变量或者全局变量(比如全局变量,或者父函数作用域链上的变量)
- 打印到控制台
- 输出到屏幕
- 写入到文件
- 写入到网络
- 触发任何外部进程
- 调用任何具有副作用的其他函数
代码示例
函数式编程在很大程度上避免了副作用,它使程序看起来更容易理解,并且更容易测试
示例1
let default = '';
const run = ()=>{
default = '1111;
return 'aaaaaaaa';
}
run();
示例2
let counter = 0;
const sum = (a,b)=>{
counter++;
return a+b;
}
引用透明
定义
在函数式编程中,引用透明经常被定义为这样一个事实:在程序中,表达式可以被它的值(或具有相同值的任何东西)替换,而不改变程序的结果。这意味着对于给定的参数总是返回相同的结果,而不会产生任何副作用。
代码示例
示例1
int add(int a, int b) {
return a + b
}
int mult(int a, int b) {
return a * b;
}
// 1) 表达式是add(2, mult(3, 4)).
// 2) 如果我们替换成mult(3,4) ,它的结果也是12.
// 3) 我们可以使add(2, 12) === add(2, mult(3, 4))
高阶函数
定义
高阶函数是将别的函数作为参数或者将其作为返回值的函数。
代码示例
在Javasript中,函数可以当做值(第一等公民)。这意味着他们可以被当做变量或者作为参数传递
示例——Map:
const double = n => n * 2
//这个map就是一个高阶函数
[1, 2, 3, 4].map(double); // [ 2, 4, 6, 8 ]
示例——Filter
let isBoy = student => student.sex === 'M';
//Filter是一个高阶函数
let getBoys = grades =>grades.filter(isBoy);
示例——Custom
const ivaTax = (a)=> ((a*21)/100);
//Calc是一个高阶函数
const calc = (ammount, tax) => tax(ammount);
//调用
calc(100.50,ivaTax);
一等函数
定义
我认为这个概念是非常重要的,可能它并不是来自于函数式编程,但它是很有趣的。一等函数意味着你可以像对待其他一等对象——他们可以被存储为变量,可以进行传递,被其他函数返回,甚至还能保存自己的属性。有时被称为一等公民,作为一个对象,它支持可以对其他对象进行的所有操作。
事实上,JavaScript对象本身就是一种对象类型。因此一等函数可以支持与其他对象相同的操作。
- 被存储在一个变量之中
- 传参给其他函数
- 作为返回值给其他函数
- 被存储在数据结构中
- 保存他们自己的属性和方法
代码示例
对于JavaScript程序员来讲,这意味着你可以使用强大的设计模式,例如高阶函数、回调函数等。
示例——存储在函数中
const sayHi = (name)=>console.log('HI',name);
sayHi('Damian');
示例——作为参数传递
const tax = (tax)=>(tax*21)/100;
const buy = (value,taxes)=> value+taxes(value);
//调用
buy(100,tax);
示例——存储在数据结构中
const tax = (tax)=>(tax*21)/100;
const buy = (value,taxes)=> value+taxes(value);
const scooter = {
ammount:100,
buy,
tax
};
//调用
scooter.buy(scooter.value,scooter.tax);
递归
定义
递归是一种迭代操作的技术,通过不断的调用它自己直到得到最终的结果。大多数的循环都可以改写为递归风格,在一些函数式语言中,这是默认的循环方式。
代码示例
JavaScript确实支持递归函数,但我们需要知道的是大多数的JavaScript编译器目前还没有优化来安全的支持它们。
在JS中,我们可以使用filter,map,reduce,foreach这些高阶函数来使用递归。
代码示例1
const countdown = (value) => (value>0)?countdown(value-1):value;
代码示例2
const factorial = (number) => (number<=0)?1:(number*factorial(number-1));
注意
使用此函数有可能引起JS引擎的调用堆栈中造成溢出。
RangeError: Maximum call stack size exceeded:
countdown(100000)
Uncaught RangeError: Maximum call stack size exceeded
at countdown (<anonymous>:1:19)
at countdown (<anonymous>:1:40)
at countdown (<anonymous>:1:40)
at countdown (<anonymous>:1:40)
at countdown (<anonymous>:1:40)
at countdown (<anonymous>:1:40)
at countdown (<anonymous>:1:40)
at countdown (<anonymous>:1:40)
at countdown (<anonymous>:1:40)
at countdown (<anonymous>:1:40)
解决方案——尾递归
有一种叫尾递归的技术可以解决这种情况
这里有一些相关的文章:
eddmann.com/posts/recur… medium.com/@cukejianya… en.wikipedia.org/wiki/Trampo… raganwald.com/2013/03/28/…
代码示例
//尾递归
const trampoline = (fn) => {
while (typeof fn === 'function') {
fn = fn();
}
return fn;
};
//测试
const odd = (n) => () => n === 0 ? false : even(n - 1);
const even = (n) => () => n === 0 ? true : odd(n - 1);
//调用
trampoline(factorial(100000)) //
闭包
闭包是捆绑在一起(封闭)的函数与对其周围状态(词法环境)的引用的组合。换句话说,闭包可以使你从内部函数访问外部函数的作用域。在JavaScript中,每当函数创建时,闭包都会被创建。
无状态
定义
无状态编程是一种范式,在这种范式中,您实现的操作(函数、方法、过程,无论怎么称呼它们)对计算的状态不敏感。这意味着操作中使用的所有数据都将作为操作的输入传递,而调用该操作的任何操作所使用的所有数据都将作为输出返回。
代码示例
这是函数组合的一种形式,因为这是传递给add函数的乘法的结果。
示例
const isEven = x => x % 2 === 0
const filterOutOdd = collection => collection.filter(isEven)
const add = (x, y) => x + y
const sum = collection => collection.reduce(add)
const sumEven = collection => compose(sum, filterOutOdd)(collection)
sumEven([1, 2, 3, 4])
组合
定义
用最一般的术语来说,我们可以把整个编程原则分成几个阶段:
- 理解复杂问题的逻辑
- 将复杂问题分解为几个小的问题
- 一次解决一个小问题,再把它们组合起来,形成一个连贯的解决方案。
代码示例
这是函数组合的一种形式,因为这是传递给add函数的乘法的结果。在数学中,我们也是这样理解的:f(x)0g(x) = f(g(x)), example. f(x)=2x+5 and g(X)=1x+2 => f(g(x))=2.(1x+2)+5
示例——函数组合
const isEven = x => x % 2 === 0
const filterOutOdd = collection => collection.filter(isEven)
const add = (x, y) => x + y
const sum = collection => collection.reduce(add)
const sumEven = collection => compose(sum, filterOutOdd)(collection)
sumEven([1, 2, 3, 4])
科里化
定义
科里化是将具有多个参数的函数转换为单个参数的函数的过程。
代码示例
科里化函数是一次接受多个参数的函数。
示例——科里化1
const notCurry = (x, y, z) => x + y + z; // 常规函数
const curry = x => y => z => x + y + z; // 科里化函数
示例——科里化2
const divisible = mod => num => num % mod;
//调用
divisible(10)(2);
//调用
const divisibleEn3 = divisible(3);
divisibleEn3(10)
Lambda
定义
Lambda表达式出现在大多数现代编程语言中(Python, Ruby, Java...),它们是创建函数的简单表达式。这对编程语言是非常重要的,因为它支持一级函数,这基本上意味着将函数作为参数传递给其他函数或将其分配给变量。
什么是Lambda?
在计算机科学中,lambda表达式最重要的特性是它被用作数据。也就是说函数能够作为参数被传递给另一个函数,作为函数的返回值,或者被分配给变量或者数据结构。这是编程语言中的一个经典,当我们谈论lambda时使用=>或->simbols。
Lambda vs 匿名函数
匿名函数顾名思义就是没有名称的函数。
const hello = function(name){console.log('Hi '+name);}
Or using arrow functions
const hello = (name) => {console.log('Hi '+name);}
Other example:
[1,2,3,4,6].filter((num)=>num>=3); //(num)=>num>=3 是一个匿名函数
lambda函数是作为参数使用的函数:
[1,2,3,4,5,6,7,8].map(e=>({value:e})); //e=>({value:e})这是一个Lambad
$('#el').on('click',()=>{.....}); //是一个lambda,是一个匿名函数,也是一个箭头函数
$('#el').on('click',function(){.....}); //是一个lambda,也是一个匿名函数
$('#el').on('click',function clickHandler()=>{.....}); //是一个lambda,也是一个具名函数
代码示例
Lambda不总是匿名函数,这看上去会让人疑惑,因为你将lambda等于于匿名函数当做真理。虽然这是一个有用的概念,匿名函数有它有趣的用法,比如points-free-style是一个有用的概念,但这并不是lambda表达式的主要的地方,它主要是一个语法糖。
示例——箭头函数
const isChildren = (person)=>person.age<18;
const isTeen = (person)=>person.age>18 and person.age<25;
const course = [{name:'bob',age:18},{name:'Damian',age:32},{name:'Alexander',age:19}];
//Get childrens.
course.filter(person=>isChildren(person));
//Get teens.
course.filter(person=>isTeen(person));
示例——具名函数
$('#el').on('click',function clickHandler()=>{.....}); //Lambda表达式
示例——匿名函数
$('#el').on('click',()=>{.....});