函数式编程(FP)就是将所有计算都当作纯函数,没有任何副作用,没有任何突变的编程泛型
基础介绍
以函数式编程中比较典型的例子来描述吧——海鸥程序。鸟群合并则可以变成了一个更大的鸟群,繁殖则增加了鸟群的数量,增加的数量就是它们繁殖出来的海鸥的数量。
这个程序并不是面向对象的良好实践,它只是强调当前这种变量赋值方式的一些弊端。
var Flock = function(n) {
this.seagulls = n;
};
Flock.prototype.conjoin = function(other) {
this.seagulls += other.seagulls;
return this;
};
Flock.prototype.breed = function(other) {
this.seagulls = this.seagulls * other.seagulls;
return this;
};
var flock_a = new Flock(4);
var flock_b = new Flock(2);
var flock_c = new Flock(0);
var result = flock_a.conjoin(flock_c).breed(flock_b).conjoin(flock_a.breed(flock_b)).seagulls;
//=> 32
按照实现的预期,我们期望答案是16,但是输出的结果却是32,这样的代码形式(用原型来定义方法)会使得代码的内部状态变得非常难以追踪,错误的原因在于flock_a 在运算过程中永久地改变了。(改变了this指向的值)
我们一开始conjoin(+0)的时候a还没有发生变化,但是breed(*2)后,a的值就发生变化,a此时是8。
再换一种方式:
var conjoin = function(flock_x, flock_y) {
return flock_x + flock_y
};
var breed = function(flock_x, flock_y) {
return flock_x * flock_y
};
var flock_a = 4;
var flock_b = 2;
var flock_c = 0;
var result = conjoin(breed(flock_b, conjoin(flock_a, flock_c)), breed(flock_a, flock_b));
虽然得到了正确的答案,而且少写了很多代码。但函数嵌套有点让人费解,不过代码肯定是越直白越好,所以如果我们再深入挖掘,我们会发现,它不过是在进行简单的加(conjoin) 和乘(breed)运算而已。 代码中的两个函数除了函数名有些特殊,其他没有任何难以理解的地方。我们把它们重命名一下,看看它们的真实功能。
var add = function(x, y) { return x + y };
var multiply = function(x, y) { return x * y };
var flock_a = 4;
var flock_b = 2;
var flock_c = 0;
var result = add(multiply(flock_b, add(flock_a, flock_c)), multiply(flock_a, flock_b));
//=>16
这样,我们会发现,我们所做的无非是简单的加减乘除:
// 结合律(assosiative)
add(add(x, y), z) == add(x, add(y, z));
// 交换律(commutative)
add(x, y) == add(y, x);
// 同一律(identity)
add(x, 0) == x;
// 分配律(distributive)
multiply(x, add(y,z)) == add(multiply(x, y), multiply(x, z));
基于此,我们可以针对原有代码进行处理:
// 原有代码
add(multiply(flock_b, add(flock_a, flock_c)), multiply(flock_a, flock_b));
// 应用同一律,去掉多余的加法操作(add(flock_a, flock_c) == flock_a)
add(multiply(flock_b, flock_a), multiply(flock_a, flock_b));
// 再应用分配律
multiply(flock_b, add(flock_a, flock_a));
这样,我们就可以通过简单的函数逻辑调整,优化了绝大多数的代码量,只需要定义好add和multiply即可。
函数式编程具有两个基本特征。
- 函数是第一等公民
- 函数是纯函数
函数是一等公民
概括
函数是“一等公民”实际上说的是它们和其他对象都一样。换句话说,函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值,可以把它们存在数组里等等。
// 赋值
var a = function fn1() { }
var b = [() => {console.log(1)}];
// 函数作为参数
function fn2(fn) { fn() }
// 函数作为返回值
function fn3() { return function() {} }
那来看看这个例子:
const hi = name => `Hi ${name}`;
const hello = (name) => hi(name);
这里 hello 指向的那个把 hi 包了一层的包裹函数完全是多余的。为什么?
因为 JavaScript 的函数是可调用的,当 hi 后面紧跟 () 的时候就会运行并返回一个值;如果没有 (),hi 就简单地返回存到这个变量里的函数。我们来确认一下:
hi; // name => `Hi ${name}`
hi("jonas"); // "Hi jonas"
hello 只不过是转了个身然后以相同的参数调用了 hi 函数而已,因此我们可以这么写:
const hello = hi;
hello("times"); // "Hi times"
换句话说,hi 已经是个接受一个参数的函数了,而它仅仅是用这个相同的参数调用 hi,这样完全多此一举。 用一个函数把另一个函数包起来,目的仅仅是延迟执行,真的是非常糟糕的编程习惯。 接下来看几个例子:
// bad
const getServerStuff = callback => ajaxCall(json => callback(json));
// good
const getServerStuff = ajaxCall;
等同于
ajaxCall(json => callback(json));
// 等价于
ajaxCall(callback);
// 那么,重构下 getServerStuff
const getServerStuff = callback => ajaxCall(callback);
// ...就等于
const getServerStuff = ajaxCall // 看,没有括号
所以以上才是写函数的正确方式.
再来一个例子(实现博客的一个功能):
const BlogController = {
index(posts) { return Views.index(posts); },//首页功能
show(post) { return Views.show(post); },
create(attrs) { return Db.create(attrs); },
update(post, attrs) { return Db.update(post, attrs); },
destroy(post) { return Db.destroy(post); },
};
这个Controller 99% 的代码都是垃圾,可以重写为:
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
};
或者直接全部删掉,因为它的作用仅仅就是把视图(Views)和数据库(Db)打包在一起而已。
const index=BlogController.index;//等同于 const index = views.index
为什么函数是一等公民
回顾前面的代码, getServerStuff
和BlogController
,虽说添加一些没有实际用处的间接层实现起来很容易,但这样做除了徒增代码量,提高维护和检索代码的成本外,没有任何用处。 另外,如果一个函数被不必要地包裹起来了,而且发生了改动,那么包裹它的那个函数也要做相应的变更。
httpGet('/post/2', json => renderPost(json));
如果 httpGet 要改成可以抛出一个可能出现的 err 异常,那我们还要回过头去把“胶水”函数也改了。
// 把整个应用里的所有 httpGet 调用都改成这样,可以传递 err 参数。
httpGet('/post/2', (json, err) => renderPost(json, err));
写成一等公民函数的形式,要做的改动将会少得多:
httpGet('/post/2', renderPost); // renderPost 将会在 httpGet 中调用,想要多少参数都行
除了删除不必要的函数,正确地为参数命名也必不可少。当然命名不是什么大问题,但还是有可能存在一些不当的命名,尤其随着代码量的增长以及需求的变更,这种可能性也会增加。 项目中常见的一种造成混淆的原因是,针对同一个概念使用不同的命名。还有通用代码的问题。比如,下面这两个函数做的事情一模一样,但后一个就显得更加通用,可重用性也更高:
// 只针对当前的博客
const validArticles = articles =>
articles.filter(article => article !== null && article !== undefined),
// 对未来的项目更友好
const compact = xs => xs.filter(x => x !== null && x !== undefined);
在命名的时候,我们特别容易把自己限定在特定的数据上(本例中是 articles
)。这种现象很常见,也是重复造轮子的一大原因。 有一点我必须得指出,你一定要非常小心 this 值,这一点与面向对象代码类似。如果一个底层函数使用了 this,而且是以一等公民的方式被调用的,那就很容易掉进this的坑里(课上代码已经讲的足够多了)。
var fs = require('fs');
// bad
fs.readFile('freaky\_friday.txt', Db.save);
// good a little
fs.readFile('freaky\_friday.txt', Db.save.bind(Db));
把 Db 绑定(bind)到它自己身上以后,你就可以随心所欲地调用它的原型链式了。
this 有利有弊,如果不熟悉,尽量避免使用它,因为在函数式编程中根本用不到它。然而,在使用其他的类库时,可能会发现各种各样关于this的使用。
纯函数的好处
什么是纯函数
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
比如数组的 slice
和 splice
,这两个函数的作用并无二致——但是注意,它们各自的方式却大不同,但不管怎么说作用还是一样的。
slice
符合纯函数的定义:因为对相同的输入它保证能返回相同的输出;splice
却不同:会产生可观察到的副作用,即这个数组永久地改变了;
var xs = [1,2,3,4,5];
// 纯的 xs.slice(0,3); //[1,2,3]
xs.slice(0,3); //[1,2,3]
xs.slice(0,3); //[1,2,3]
// 不纯的 xs.splice(0,3); //[1,2,3]
xs.splice(0,3); //[4,5]
xs.splice(0,3); //[]
在函数式编程中,我们追求的是那种可靠的,每次都能返回同样结果的函数,而不是像 splice
这样每次调用后都把数据弄得一团糟的函数。
来看看另一个例子:
// 不纯的
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// 纯的
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
在不纯的版本中,checkAge
的结果将取决于 minimum
这个可变变量的值。换句话说,它取决于系统状态(system state
);因为它引入了外部的环境,从而增加了认知负荷(cognitive load
)。 这个例子可能还不是那么明显,但这种依赖状态是影响系统复杂度的罪魁祸首。输入值之外的因素能够左右 checkAge
的返回值,不仅让它变得不纯,而且导致每次我们思考整个软件的时候都痛苦不堪。 另一方面,使用纯函数的形式,函数就能做到自给自足。我们也可以让 minimum
成为一个不可变(immutable
)对象,这样就能保留纯粹性,因为状态不会有变化。要实现这个效果,必须得创建一个对象,然后调用 Object.freeze
方法:
var immutableState = Object.freeze({
minimum: 21
});
副作用内容
“副作用”的关键部分在于“副”。就像一潭死水中的“水”本身并不是幼虫的培养器,“死”才是生成虫群的原因。同理,副作用中的“副”是滋生 bug 的温床。 副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
副作用可能包含,但不限于:
- 更改文件系统;
- 往数据库插入记录;
- 发送一个 http 请求;
- 可变数据;
- 打印/log;
- 获取用户输入;
- DOM 查询;
- 访问系统状态…
概括来讲,只要是跟函数外部环境发生的交互就都是副作用——这一点可能会让你怀疑无副作用编程的可行性。函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。 这并不是说,要禁止使用一切副作用,而是说,要让它们在可控的范围内发生。 副作用让一个函数变得不纯是有道理的:从定义上来说,纯函数必须要能够根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,那么就无法保证这一点了。
demo
函数是两种数值之间的关系:输入和输出。尽管每个输入都只会有一个输出,但不同的输入却可以有相同的输出。下图展示了一个合法的从 x 到 y 的函数关系;
下面这张图表展示的就不是一种函数关系,因为输入值 5 指向了多个输出:
函数可以描述为一个集合,这个集合里的内容是 (输入, 输出) 对:[(1,2), (3,6), (5,10)]。 或者:
var toLowerCase = {"A":"a", "B": "b", "C": "c", "D": "d", "E": "e", "D": "d"};
toLowerCase["C"]; // "c"
var isPrime = {1:false, 2: true, 3: true, 4: false, 5: true, 6:false};
isPrime[3]; // true
当然实际情况中,可能需要进行一些计算而不是手动指定各项值;不过上例倒是表明了另外一种思考函数的方式。 从数学的概念上将,纯函数就是数学上的函数,而且是函数式编程的全部。使用这些纯函数编程能够带来大量的好处,让我们来看一下为何要不遗余力地保留函数的纯粹性的原因。
追求纯函数的原因
可缓存性
首先,纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 memoize 技术:
var squareNumber = memoize(function(x){ return x*x; });
//原理上只用把参数和对应的结果数据存到一个对象中,调用时,判断参数对应的数据是否存在,存在就返回对应的结果数据。
var memoize = function(f) {
var cache = {};//缓存存到这
/*
* cache={
* '4':16
* '5':25
* }
*/
return function() {
var arg_str = JSON.stringify(arguments);
cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
return cache[arg_str];
};
};
// 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次
squareNumber(4);//16
squareNumber(4); // 从缓存中读取输入值为 4 的结果 16
squareNumber(5); // 25
squareNumber(5);// 从缓存中读取输入值为 5 的结果 25
值得注意的一点是,可以通过延迟执行的方式把不纯的函数转换为纯函数:
var pureHttpCall = memoize(function(url, params){
return function() {
return $.getJSON(url, params);
}
});
这里有趣的地方在于我们并没有真正发送 http 请求——只是返回了一个函数,当调用它的时候才会发请求。这个函数之所以有资格成为纯函数,是因为它总是会根据相同的输入返回相同的输出:给定了 url 和 params 之后,它就只会返回同一个发送 http 请求的函数。 memoize
函数工作起来没有任何问题,虽然它缓存的并不是 http 请求所返回的结果,而是生成的函数。
可移植性/自文档化(Protable/self-Documenting)
纯函数是完全自给自足的,它需要的所有东西都能轻易获得。仔细思考思考这一点…这种自给自足的好处是什么呢?首先,纯函数的依赖很明确,因此更易于观察和理解。
// 不纯的
var signUp = function(attrs) {
var user = saveUser(attrs);
welcomeUser(user);
};
var saveUser = function(attrs) {
var user = Db.save(attrs);
...
};
var welcomeUser = function(user) {
Email(user, ...);
...
};
// 纯的
var signUp = function(Db, Email, attrs) {
return function() {
var user = saveUser(Db, attrs);
welcomeUser(Email, user);
};
};
var saveUser = function(Db, attrs) {
...
};
var welcomeUser = function(Email, user) {
...
};
这个例子表明,纯函数对于其依赖必须要明确,这样我们就能知道它的目的。 仅从纯函数版本的 signUp 的签名就可以看出,它将要用到 Db、Email 和 attrs,这在最小程度上给了我们足够多的信息。 其次,通过强迫“注入”依赖,或者把它们当作参数传递,我们的应用也更加灵活;因为数据库或者邮件客户端等等都参数化了。如果要使用另一个 Db,只需把它传给函数就行了。如果想在一个新应用中使用这个可靠的函数,尽管把新的 Db 和 Email 传递过去就好了,非常简单。 命令式编程中“典型”的方法和过程都深深地根植于它们所在的环境中,通过状态、依赖和有效作用(available effects)达成;纯函数与此相反,它与环境无关,只要我们愿意,可以在任何地方运行它。
可测试性
纯函数让测试更加容易。我们不需要伪造一个“真实的”支付网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。
合理性
很多人相信使用纯函数最大的好处是引用透明性。如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。 由于纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果,这也就保证了引用透明性。我们来看一个例子:
var Immutable = require('immutable');
var decrementHP = function(player) {
return player.set("hp", player.hp-1);
};
var isSameTeam = function(player1, player2) {
return player1.team === player2.team;
};
var punch = function(player, target) {
if(isSameTeam(player, target)) {
return target;
} else {
return decrementHP(target);
}
};
var jobe = Immutable.Map({name:"Jobe", hp:20, team: "red"});
var michael = Immutable.Map({name:"Michael", hp:20, team: "green"});
punch(jobe, michael);
//=> Immutable.Map({name:"Michael", hp:19, team: "green"})
decrementHP
、isSameTeam
和 punch
都是纯函数,所以是引用透明的。我们可以使用一种叫做“等式推导”的方法来分析代码。所谓“等式推导”就是“一对一”替换,有点像在不考虑程序性执行的怪异行为的情况下,手动执行相关代码。我们借助引用透明性来剖析一下这段代码。 首先内联 isSameTeam
函数:
var punch = function(player, target) {
if(player.team === target.team) {
return target;
} else {
return decrementHP(target);
}
};
因为是不可变数据,我们可以直接把 team 替换为实际值:
var punch = function(player, target) {
if("red" === "green") {
return target;
} else {
return decrementHP(target);
}
};
if 语句执行结果为 false,所以可以把整个 if 语句都删掉:
var punch = function(player, target) {
return decrementHP(target);
};
如果再内联 decrementHP
,我们会发现这种情况下,punch 变成了一个让 hp 的值减 1 的调用:
var punch = function(player, target) {
return target.set("hp", target.hp-1);
};
等式推导带来的分析代码的能力对重构和理解代码非常重要。事实上,我们重构海鸥程序使用的正是这项技术:利用加和乘的特性。
并行代码
最后一点,也是决定性的一点:我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition
)。 并行代码在服务端 js 环境以及使用了 web worker 的浏览器那里是非常容易实现的,因为它们使用了线程(thread
)。不过出于对非纯函数复杂度的考虑,当前主流观点还是避免使用这种并行。
柯里化
什么是柯里化
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。(只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数)
你可以一次性地调用 curry 函数,也可以每次只传一个参数分多次调用。
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
这里我们定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。一次性地调用它实在是有点繁琐,好在我们可以使用一个特殊的 curry 帮助函数,使这类函数的定义和调用更加容易。
var curry = require('lodash').curry;
var match = curry(function(what, str) {
return str.match(what);
});
var replace = curry(function(what, replacement, str) {
return str.replace(what, replacement);
});
var filter = curry(function(f, ary) {
return ary.filter(f);
});
var map = curry(function(f, ary) {
return ary.map(f);
});
在上面的代码中遵循的是一种简单,同时也非常重要的模式。即策略性地把要操作的数据(String, Array)放到最后一个参数里。到使用它们的时候你就明白这样做的原因是什么了。
match(/\s+/g, "hello world");//正则 /\s+/g 空格
// [ ' ' ]
match(/\s+/g)("hello world");
// [ ' ' ]
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
hasSpaces("hello world");
// [ ' ' ]
hasSpaces("spaceless");
// null
filter(hasSpaces, ["tori_spelling", "tori amos"]);
// ["tori amos"]
var findSpaces = filter(hasSpaces);
// function(xs) { return xs.filter(function(x) { return x.match(/\s+/g) }) }
findSpaces(["tori_spelling", "tori amos"]);
// ["tori amos"]
var noVowels = replace(/[aeiou]/ig);
// function(replacement, x) { return x.replace(/[aeiou]/ig, replacement) }
var censored = noVowels("*");
// function(x) { return x.replace(/[aeiou]/ig, "*") }
censored("Chocolate Rain");
// 'Ch*c*l*t* R**n'
这里表明的是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。
使用用途
curry 的用处非常广泛,就像在 hasSpaces、findSpaces 和 censored 看到的那样,只需传给函数一些参数,就能得到一个新函数。 用 map 简单地把参数是单个元素的函数包裹一下,就能把它转换成参数为数组的函数。
var getChildren = function(x) {
return x.childNodes;
};
var allTheChildren = map(getChildren);
只传给函数一部分参数通常也叫做局部调用,能够大量减少样板文件代码。考虑上面的 allTheChildren
函数,如果用 lodash [一个著名的javascript原生库,不需要引入其他第三方依赖。很多方法lodash已经帮你写好了,直接调用就行,不用自己费尽心思去写了,而且可以统一方法的一致性。Lodash使用了一个简单的 _
符号。]的普通 map 来写会是什么样的(注意参数的顺序也变了):
不了解lodash的可以先看看这个:Lodash ———— 入门篇
var allTheChildren = function(elements) {
return _.map(elements, getChildren);
};
通常我们不定义直接操作数组的函数,因为只需内联调用 map(getChildren)
就能达到目的。这一点同样适用于 sort、filter 以及其他的高阶函数(高阶函数:参数或返回值为函数的函数)。
当我们谈论纯函数的时候,我们说它们接受一个输入返回一个输出。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出啊。 哪怕输出是另一个函数,它也是纯函数。当然 curry 函数也允许一次传递多个参数,但这只是出于减少 () 的方便。
// 示意而已
function ajax(type, url, data) {
var xhr = new XMLHttpRequest();
xhr.open(type, url, true);
xhr.send(data);
}
// 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余
ajax('POST', '[www.test.com](http://www.test.com)', "name=xianzao")
ajax('POST', '[www.test2.com](http://www.test2.com)', "name=xianzao")
ajax('POST', '[www.test3.com](http://www.test3.com)', "name=xianzao")
// 利用 curry
var ajaxCurry = curry(ajax);
// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('[www.test.com](http://www.test.com)', "name=xianzao");
// 以 POST 类型请求来自于 [www.test.com](http://www.test.com) 的数据
var postFromTest = post('[www.test.com](http://www.test.com)');
postFromTest("name=xianzao");
可以看到:curry 的用途包括参数复用。本质上是降低通用性,提高适用性。
再举个例子:
var person = [{name: 'xianzao'}, {name: 'zaoxian'}]
如果我们要获取所有的 name 值,可以使用map:
var name = person.map(function (item) {
return item.name;
})
但如果有curry函数
var prop = curry(function (key, obj) {
return obj[key]
});
var name = person.map(prop('name'))
person.map(prop('name'))
是 person 对象遍历(map)获取(prop) name 属性.
代码组合
什么是代码组合?
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
f 和 g 都是函数,x 是在它们之间通过“管道”传输的值。 组合看起来像是在饲养函数。你就是饲养员,选择两个有特点且你喜欢的函数,让它们结合,产下一个崭新的函数。组合的用法如下:
var toUpperCase = function(x) {
return x.toUpperCase();
};
var exclaim = function(x) {
return x + '!';
};
var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
代码组合:两个函数组合之后返回了一个新函数,也就是组合某种类型(本例中是函数)的两个元素本就该生成一个该类型的新元素。在 compose 的定义中,g 将先于 f 执行,因此就创建了一个从右到左的数据流。 这样做的可读性远远高于嵌套一大堆的函数调用,如果不用组合,shout 函数将会是这样的:
var shout = function(x){
return exclaim(toUpperCase(x));
};
让代码从右向左运行,而不是由内而外运行,我们来看一个顺序很重要的例子:
var head = function(x) {
return x[0];
};
var reverse = reduce(function(acc, x){
return [x].concat(acc);
}, []);
var last = compose(head, reverse);
last(['jumpkick', 'roundhouse', 'uppercut']);
//=> 'uppercut'
reverse 反转列表,head 取列表中的第一个元素;所以结果就是得到了一个 last 函数(即取列表的最后一个元素),虽然它性能不高。这个组合中函数的执行顺序应该是显而易见的。尽管我们可以定义一个从左向右的版本,但是从右向左执行更加能够反映数学上的含义。
// 结合律
var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
// true
不管你是把 g 和 h 分到一组,还是把 f 和 g 分到一组都不重要。所以,如果我们想把字符串变为大写,可以这么写:
compose(toUpperCase, compose(head, reverse));
// 或者
compose(compose(toUpperCase, head), reverse);
因为如何为 compose 的调用分组不重要,所以结果都是一样的。这也让我们有能力写一个可变的组合,用法如下:
// 前面的例子中我们必须要写两个组合才行,但既然组合是符合结合律的,我们就可以只写一个,
// 而且想传给它多少个函数就传给它多少个,然后让它自己决定如何分组。
var lastUpper = compose(toUpperCase, head, reverse);
lastUpper(['jumpkick', 'roundhouse', 'uppercut']);
//=> 'UPPERCUT'
var loudLastUpper = compose(exclaim, toUpperCase, head, reverse)
loudLastUpper(['jumpkick', 'roundhouse', 'uppercut']);
//=> 'UPPERCUT!'
运用结合律能为我们带来强大的灵活性,而且你也可以在类似 lodash、underscore 以及 ramda 这样的类库中找到它们的常规定义。 结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起。 eg:
var loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
// 或
var last = compose(head, reverse);
var loudLastUpper = compose(exclaim, toUpperCase, last);
// 或
var last = compose(head, reverse);
var angry = compose(exclaim, toUpperCase);
var loudLastUpper = compose(angry, last);
pointfree
pointfree 模式指的是,永远不必说出你的数据。它的意思是说,函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。
// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {
return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
这里所做的事情就是通过管道把数据在接受单个参数的函数间传递。 利用 curry,我们能够做到让每个函数都先接收数据,然后操作数据,最后再把数据传递到下一个函数那里去。另外注意在 pointfree 版本中,不需要 word 参数就能构造函数;而在非 pointfree 的版本中,必须要有 word 才能进行一切操作。 再来看一个例子:
// 非 pointfree,因为提到了数据:name
var initials = function (name) {
return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};
// pointfree
var initials = compose(join('. '), map(compose(toUpperCase, head)), split(' '));
initials("hunter stockton thompson");
// 'H. S. T'
pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用。对函数式代码来说,pointfree 是非常好的石蕊试验,因为它能告诉我们一个函数是否是接受输入返回输出的小函数。比如,while 循环是不能组合的。不过你也要警惕,pointfree 就像是一把双刃剑,有时候也能混淆视听。并非所有的函数式代码都是 pointfree 的,不过这没关系。可以使用它的时候就使用,不能使用的时候就用普通函数。
debug
组合的一个常见错误是,在没有局部调用之前,就组合类似 map 这样接受两个参数的函数。
// 错误做法:我们传给了 `angry` 一个数组,根本不知道最后传给 `map` 的是什么东西。
var latin = compose(map, angry, reverse);
latin(["frog", "eyes"]);
// error
// 正确做法:每个函数都接受一个实际参数。
var latin = compose(map(angry), reverse);
latin(["frog", "eyes"]);
// ["EYES!", "FROG!"])
如果在 debug 组合的时候遇到了困难,那么可以使用下面这个实用但不纯的 trace 函数来追踪代码的执行情况。
var trace = curry(function(tag, x){
console.log(tag, x);
return x;
});
var dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' '));
dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined
toLower 的参数是一个数组,所以需要先用 map 调用一下它。
var dasherize = compose(join('-'), map(toLower), split(' '), replace(/\s{2,}/ig, ' '));
dasherize('The world is a vampire');
// 'the-world-is-a-vampire'