1 引用透明(referential transparency)与纯函数
”Referential transparency means, a function call can be replaced with it's return value and not affect any of the rest of the program.” 我们拆分开来看这句话。
🎯 ”a function call can be replaced with it's return value“ 一个函数调用可以直接被其返回值替代,即函数调用等同于其返回值。这说明,无论调用多少次,只要给定的入参不变,返回值就是不变的。就是上一篇博客说到的,“same input same output”。
function add(x, y){
return x + y
}
let result = add(1, 2) // add(1, 2) 可以直接被替换成 3,而结果不受影响
🎯 ”not affect any of the rest of the program“ 将函数调用替换成返回值后,程序其余地方不会受到影响。这说明,这个函数调用本身不存在副作用。
var z = 0
function add(x, y){
z ++
return x + y
}
let result = add(1, 2) // add(1, 2) 不可以直接被替换成 3,因为 方法调用的时候改变了全局变量 z,产生了副作用
所以,引用透明是指,一个函数的调用既符合,“same input same output”,又符合”不存在副作用“。很明显这就是我们上篇博客说的纯函数。 因此,如果一个函数存在引用透明,那么这个函数就是纯函数。
2 不可变性(Immutability)
不可变性是指在程序中,要使用不可被改变的数据。主要是为了减少程序的副作用。 这其实是针对引用类型使用过程中可能出现的bug,在我们平时开发中很常见。
例如:
let obj = { a: 1};
function add(o){
o.b = 2
return o;
}
let newObj = add(obj)
有开发经验的一眼就会发现这可能会有bug,因为obj作为引用类型传入函数add的时候,在add中没有被拷贝。 所以针对引用类型,我们要确保它的不可变性,确保原来的变量不被改变。
修改如下:
let obj = { a: 1};
function add(o){
let myObj = Object.freeze(o)
return myObj ;
}
let newObj = add(obj)
3 等式推理(equational reasoning)
一个函数返回另一个函数,并且两个函数的参数相同,可以改成下面的样子。
getPerson(function onPerson(person){
return renderPerson(person);
});
getPerson(renderPerson)
不再需要外面包裹的函数,直接调用renderPerson作为回调函数。
例如:
Promise.resolve([1]).then((res) => console.log(res)) // [1]
Promise.resolve([1]).then(console.log) // [1]
4 Point-free
Point-free 是指把多个函数组合在一起,不需要考虑值的传递,只考虑函数本身逻辑与运算的编程风格。
例如:
function isShort(str){
return str.length <= 5
}
function not(fn){
return function neg(args){
return !fn(args)
}
}
not(isShort)('123456')
isShort函数用来判断字段长度是不是短。not函数用来对函数取反。两个函数组合在一起不需要在定义中提起过程中值的流动。
这里有函数组合的结构,对应在函数式编程是函数compose。为了更好地理解 Point-free,我们先来理解下 compose 和 pipe。
🎯 compose 和 pipe
compose是让函数从右到左执行,而pipe是从左到右执行函数。先看pipe,因为它从左到右的执行顺序和我们得阅读习惯更符合。
来看个题: 已知现有三个函数,求 (10+1)*8/2 后的结果。
function increment(x){
return x + 1
}
function multi(x){
return x * 8
}
function mod(x){
return x / 2
}
我第一反应写成了这样令人头疼的层层嵌套的形式。
mod(multi(increment(10)))
有没有更好的办法把它扁平化处理?有的,让reduce来帮忙。
[increment, multi, mod].reduce((result, fn) => {
return fn(result)
}, 10)
上述形式被复用为函数,就成了pipe:
function pipe(...fns){
return function piped(arg){
return fns.reduce((result, fn) => fn(result), arg)
}
}
于是用 pipe 求值可以写成这样:
pipe(increment,multi,mod)(10)
compose 和 pipe 执行顺序相反,它对应的函数自然就是:
function compose(...fns){
return function composed(arg){
return fns.reduceRight((result, fn) => fn(result), arg)
}
}
// 或者
function compose(...fns){
return pipe(...fns.reverse())
}
于是用 compose 求值可以写成这样:
compose(mod, multi, increment)(10)
了解了compose 和 pipe,我们再重新回到开始的例子。它可以被写成这样更清晰、更易复用的形式。
function compose(...fns){
return function composed(arg){
return fns.reduceRight((result, fn) => fn(result), arg)
}
}
function isShort(str){
return str.length <= 5
}
function not(args){
return !args
}
compose(not, isShort)('123456')
这样point-free风格也就更加直观,即多个函数组合在一起,只考虑函数逻辑,而不用处理值的传递。
5 惰性求值
惰性求值是指,在需要的时候才去求值,而不是页面一进来就调用函数求值。
来看代码:
function repeat1(count){
let str = ''.padStart(count, 'a')
return function getAs(){
return str
}
}
let a1 = repeat1(10)
a1()
a1()
第7行已经调用padStart()并给str赋值了。第8、9行只是str的获取。
如果代码第2行换成操作量大的代码,并且获取repeat1()实例后,从未被执行,或者在我还没用到的时候就执行了,就导致了性能的浪费。于是改成下面惰性求值的形式:
function repeat2(count){
return function getAs(){
return ''.padStart(count, 'a')
}
}
let a2 = repeat2(10)
a2() // make the string
a2() // again
优点:避免调用不需要的函数,以免浪费性能。
缺点:每次都需要再生成一次string,如果调用量大不推荐。
针对“每次都需要再生成一次string”这个问题,在函数式编程常用库 lodash 或者 rambda 中,可有专门函数 memoize 来解决这个问题:
function repeater3(count){
return memoize(function getAs(){
return "".padStart(count, "a")
})
}
var a3 = repeater3(10);
a3();
a3();
memoize 对于某一函数的同样的参数,不需要再计算立刻给出结果。
在函数式编程中,经常会用柯里化(currying) 和 偏函数(partial) 进行惰性求值。
🎯 currying 和 partial
currying 和 partial 都是将多个参数的函数转化成携带部分参数的函数,以此实现惰性求值。
不同的是,currying一次接受一个参数;而partial 初始预设参数,并在后面调用中获取余下的参数。
例如,要对 function ajax(url, data, cb){} 进行改造:
partial:
function ajax(url, data, cb){}
let getInfo = partial(ajax, API)
let getCurUser = partial(getInfo, {id: 1})
getCurUser(renderUser)
// 或者
getInfo({id: 1}, renderUser)
currying:
function ajax(url, data, cb){}
let ajax = curry(3, ajax)
let getInfo = ajax(API)
let getCurUser = getInfo({id: 1})
// 或者
ajax(API)({id: 1})(renderUser)
🎯 最后,做一个练习:柯里化如下代码。
[0,2,4].map(v => {
return v + 1
})
思路演变:
function add(x, y) { return x + y; }
[0,2,4].map(v => {
return add(v, 1);
})
// 柯里化 =>
function add(x, y) { return x + y; }
add = curry(add);
[0,2,4].map(v => {
return add(1)(v);
})
// 等式推理 =>
function add(x, y) { return x + y; }
add = curry(add);
[0,2,4].map(add(1)) // Point-free