组合函数就像堆乐高积木一样,将不同的组件按照不同的方式拼成一个有用的组件。
一.输出到输入
上一章我们已经看到过一些组合函数,比如 unary(adder(3)), 这将2个函数组合到一起,其数据流如下:
functionValue <-- unary <-- adder <-- 3
3
是 adder(..)
的输入值, adder(..)
的输出值 又是 unary(..)
的输入值, unary(..)
的输出值为 functionValue
, 这叫做 unary(..)
和 adder(..)
的组合。 这种组合就像传送带一样,比如巧克力的生产: 冷却 --> 切割 --> 包装。
1.命令式函数示例
function words(str) {
return String(str)
.toLowerCase()
.split(/\s|\b/) // 按照空格或单词边界拆分
.filter(v => /^[\w]+$/.test(v)) //按单词过滤
}
function unique(list) {
var uniqList = [];
for (let i = 0; i < list.length; i++) {
if (uniqList.indexOf(list[i]) === -1) {
uniqList.push(list[i]);
}
}
return uniqList;
}
var text = "To compose two functions together, pass the \
output of the first function call as the input of the \
second function call.";
var wordsFound = words( text );
var wordsUsed = unique( wordsFound );
wordsUsed;
// ["to","compose","two","functions","together","pass",
// "the","output","of","first","function","call","as",
// "input","second"]
我们将 words(..)
的输出值命名为 wordsFound
,同时作为 unique
的输入值。
我们可以将中间过程wordsFound
省略掉,直接写为:
var wordsUsed = unique( words(text) )
这样变为一个组合函数,注意传统的函数 left-to-right, 但是组合函数一般都是 right-to-left 或者 inner-to-outer.
我们可以将上面的函数进一步的进行封装:
function uniqueWords(str) {
return unique( words(str) );
}
// 这样就可以不再写
var wordsUsed = unique( words(text) )
// 而是直接写为
var wordsUsed = uniqueWords(text);
2.Machine Making
上面的例子就像我们巧克力厂设备组合在一起,去掉了中间过程(冷却,切割,包装),直接输入一个值就输出一个值。下面我们进一步的"生产"一种设备,这个设备能够直接制造出向uniqueWords
这样的设备。
function compose2(fn2, fn1) {
return function composed(oriValue) {
return fn2( fn1(oriValue) );
}
};
// ES6写法
const compose2 =
(fn2, fn1) =>
oriValue =>
fn2( fn1(oriValue) );
下面我们"生产" uniqueWords
这种设备:
var uniqueWords = compose2(unique, words);
var wordsUsed = uniqueWords(text);
3.组合变形
<-- unique <-- words
这种组合似乎是这两个函数唯一组合顺序,但其实我们可以像乐高积木一样改变2个组件的组合方式来改变其功能。
var letters = compose2(words, unique);
var chars = letters("How are you Henry?");
chars;
// ["h","o","w","a","r","e","y","u","n"]
这个例子虽然比较巧合,但是这里是为了说明函数的组合不是一直是单向的
二.通用组合
如果我们可以将2个函数组合在一起,我们可以将任意多个函数组合在一起,通用数据流如下:
finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- oriValue
1.通用实现
function compose(...fns) {
return function composed(result) {
// 赋值上面的函数数组
var lists = fns.slice();
while (list.length > 0) {
// 将最后一个函数从数组中取出, pop()
// 并且执行它
result = list.pop()(result);
}
return result;
};
}
// ES6
const compose =
(...fns) =>
result => {
var lists = fns.slice();
while (lists.length > 0) {
result = lists.pop()(result);
}
return result;
};
2.示例
比如我们在 unique(words(text))
的基础上加一层过滤条件, 对单词长度小于4的进行过滤,即 skipShortWords(unique(words(text)))
function skipShortWords(list) {
var filteredList = [];
for (let i = 0; i < list.length; i++) {
if (list[i].length > 4) {
filteredList.push(list[i]);
}
}
return filteredList;
}
下面使用compose方法:
var biggerWords = compose(skipShortWords, unique, words);
var wordsUsed = biggerWords(text);
wordsUsed;
// ["compose","functions","together","output","first",
// "function","input","second"]
3.结合partialRight()
使用 partialRight() 我们可以做一些更有意思的事,比如先指定后面的2个参数 unique(..)
和 words(..)
// skipShortWords中使用 "<= 4" 代替 "> 4"
// 改名为skipLongWords
function skipLongWords(list) { /* ... */}
// 先指定后面的2个参数
function filterWords = partialRight(compose, unique, words);
var biggerWords = filterWords( skipShortWords );
// ["compose","functions","together","output","first",
// "function","input","second"]
var smallerWords = filterWords( skipLongWords );
// ["to","two","pass","the","of","call","as"]
partialRight让我们能够指定compose后面的参数,后续步骤(或逻辑)可以根据自己的需求进行书写。
同样我们可以使用 curry
对组合函数进行控制,比如:
// 一般我们先将compose的参数反过来,这样就可以
// fn1 --> fn2--> ...
curry( reverseArgs(compose), 3)(words)(unique)(..)
三.使用Reduce实现通用组合
我们可能永远不会在项目中自己实现compose(..), 用么使用一些工具库帮助我们实现, 但是理解它的原理能够有效的帮助我们更好的函数编程。
1.使用reduce
这种实现方式更加的简洁,使用函数编程中常用的 reduce 函数
function compose(...fns) {
return function composed(result) {
// 此时fns.reverse()数组为
// [fn1, fn2, ..., fnn]
return fns.reverse().reduce(function reducer(result, fn) {
return fn(result);
}, result);
};
}
// reduce函数
// fn1(result) --> fn2( fn1(result) ) --> ...
// 这样嵌套下去
// ES6
const compose =
(...fns) =>
result =>
fns.reverse().reduce(
(result, fn) => fn(result),
result
);
2.首次调用传入多个参数的情况
上面的函数我们可以知道,每次传入的参数都只能为1个,这样大大的限制了函数的能力,如果函数都是一元的这样无所谓,但是对多远的就不适用。
function compose(...fns) {
// reduce写在前面, 等待所有函数全部传入完毕后计算
return fns.reverse().reduce(function reducer(fn1, fn2) {
return function composed(...args) {
return fn2( fn1(...args) );
};
});
}
// ES6
const compose =
(...fns) =>
fns.reverse().reduce(
(fn1, fn2) =>
(...args) =>
fn2( fn1(...args) );
);
这种方式,先调用reduce,然后将所有的组合组合完成之后做一次性的计算;前面的compose都是传入一个函数就计算出结果,然后将结果作为下一次的输入值
3.使用递归来实现compose
递归形式为:
compose( compose(fn1, fn2, ..., fnN-1), fnN );
实现递归:
function compose(...fns) {
// 取出最后面的2个实参
// rest = [fn3, fn4, ..., fnN]
var [fn1, fn2, ...rest] = fns.reverse();
var composedFn = function composed(...args) {
return fn2( fn1(...args) );
};
// 如果只有2个函数组合
if (rest.length === 0) {
return composedFn
}
return compose(...rest.reverse(), composedFn);
}
// ES6
const compose =
(...fns) => {
var [fn1, fn2, ...rest] = fns.reverse();
var composedFn =
(...args) =>
fn2( fn1(...args) );
if (rest.length === 0) {
return composedFn;
}
compose(...rest.reverse(), composedFn);
};
递归的实现方式思路更加的清晰
四.改变实现顺序 pipe()
上面的compose传参的顺序是 from-right-to-left, 这种顺序的优势是和书写顺序一致,有另一种 from-left-to-right 的顺序, 其本质就是,就是使用 shift() 替换 pop()。使用pipe()的优势是,按照参数执行顺序传入。
function pipe(...fns) {
return function piped(result) {
var list = fns.slice();
while (list.length > 0) {
// 取出第一个参数并执行
result = list.shift()(result);
}
return result;
}
}
// 或者直接对compose的传参顺序进行反向
// 利用reverseArgs()
const pipe = reverseArgs(compose);
所以上面的一些例子可以写为:
var biggerWords = compose( skipShortWords, unique, words);
// 使用pipe()
var biggerWords = pipe(words, unique, skipShortWords);
// 还有结合partialRight()
var filterWords = partialRight(compose, unique, words);
var filterWords = partialRight(pipe, words, unique);
总结
本章主要是谈组合,即将不同的函数结合成一个函数,也介绍了通用组合的几种实现方式。一种是只能传入一元参数,另一种是首次调用不限制传入参数的个数,以及使用递归的方法实现compose.其中也利用了上一章中实现的 partialRight
, reverseArgs
的辅助函数。另外抽象和point-free的介绍将在后续的章节中补充。
2016/10/29 14:59:35