组合函数|博客系列
# 组合函数
到了第四章了,函数式编程的魅力似乎越来越大。
对于函数式编程者,他们会将每个函数都当成是一个“部件”,在需要时通过组装不同的“部件”,来拼凑出一个自己想要的“模型”。
专业点的角度来说,就是我们能够定义某种组合方式,来让它们成为一种新的组合函数,程序中不同的部分都可以使用这个函数。这种将函数一起使用的过程叫做组合。
再介绍组合函数的概念之前,我们就已经使用过组合了。
例如在之前我们的一个案例:
unary(adder(3))
上面的表达式,我们将两个函数整合起来,然后将第一个函数调用产生的值(输出)当成第二个函数调用的实参(输入)。画个简图,也就是这样:
functionValue <-- unary <-- adder <-- 3
3 是 adder(..) 的输入。而 adder(..) 的输出是 unary(..) 的输入。unary(..) 的输出是 functionValue。 这就是 unary(..) 和 adder(..) 的组合。
# Compose2函数
为了满足上面组合函数的要求,我们可以来构造这么一个简单的函数:
function compose2(fn2,fn1) {
return function composed(origValue){
return fn2( fn1( origValue ) );
};
}
// ES6 箭头函数形式写法
var compose2 =
(fn2,fn1) =>
origValue =>
fn2( fn1( origValue ) );
它能够自动创建两个函数的组合,这和我们手动做的是一模一样的。
# Words案例
现在有这么一个需求,需要将给定的一个英文字符串,提取其中全部的英文单词,先全部转化小写,然后去除其中重复的单词。
我们可以先来创建这么2个函数:
function words(str) {
return String( str )
.toLowerCase()
.split( /\s|\b/ )
.filter( function alpha(v){
return /^[\w]+$/.test( v );
} );
}
function unique(list) {
var uniqList = [];
for (let i = 0; i < list.length; i++) {
// value not yet in the new list?
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"]
在上面的例子中,我们将该过程分为了2步来做。
并且先创建了wordsFound函数,然后将该函数的输出再传递给unique,实际上,上面的效果等同于:
var wordsUsed = unique( words(text) )
所以我们可以将其封装一层:
function uniqueWords(str) {
return unique( words( str ) );
}
var wordsUsed = uniqueWords(text)
你会发现,其实我们还可以这样写:
var uniqueWords = compose2( unique, words )
var wordsUsed = uniqueWords(text)
这样我们就成功将uniqueWords转化为了无形参的函数。
uniqueWords(..) 接收一个字符串并返回一个数组。它是 unique(..) 和 words(..) 的组合,并且满足我们的数据流向要求:
wordsUsed <-- unique <-- words <-- text
# compose函数
在上面我们构造了compose2函数,它能接收2个函数,并将2个函数从右向左的执行。
如果我们能够定义两个函数的组合,我们也同样能够支持组合任意数量的函数。任意数目函数的组合的通用可视化数据流如下:
finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- origValue
我们能够像这样实现一个通用 compose(..) 实用函数:
function compose(...fns) {
return function composed(result){
// 拷贝一份保存函数的数组
var list = fns.slice();
while (list.length > 0) {
// 将最后一个函数从列表尾部拿出
// 并执行它
result = list.pop()( result );
}
return result;
};
}
// ES6 箭头函数形式写法
var compose =
(...fns) =>
result => {
var list = fns.slice();
while (list.length > 0) {
// 将最后一个函数从列表尾部拿出
// 并执行它
result = list.pop()( result );
}
return result;
};
现在看一下组合超过两个函数的例子。回想下我们的 uniqueWords(..) 组合例子,让我们增加一个 skipShortWords(..),它将所有单词字母数大于4的提取出来:
function skipShortWords(list) {
return list.filter(str => str.length > 4)
}
让我们再定义一个 biggerWords(..) 来包含 skipShortWords(..)。我们期望等价的手工组合方式是 skipShortWords(unique(words(text))),所以让我们采用 compose(..) 来实现它:
var text = "To compose two functions together, pass the \
output of the first function call as the input of the \
second function call.";
var biggerWords = compose( skipShortWords, unique, words );
var wordsUsed = biggerWords( text );
wordsUsed;
// ["compose","functions","together","output","first",
// "function","input","second"]
现在,让我们回忆一下第 3 章中出现的 partialRight(..) 来让组合变的更有趣。我们能够构造一个由 compose(..) 自身组成的右偏函数应用,通过提前定义好第二和第三参数(unique(..) 和 words(..));我们把它称作 filterWords(..)(如下)。
然后,我们能够通过多次调用 filterWords(..) 来完成组合,但是每次的第一参数却各不相同。
function skipShortWords(list) {
return list.filter(str => str.length > 4)
}
function skipLongWords(list) {
return list.filter(str => str.length <= 4)
}
var filterWords = partialRight( compose, unique, words );
var biggerWords = filterWords( skipShortWords );
var shorterWords = filterWords( skipLongWords );
biggerWords( text );
// ["compose","functions","together","output","first",
// "function","input","second"]
shorterWords( text );
// ["to","two","pass","the","of","call","as"]
花些时间考虑一下基于 compose(..) 的右偏函数应用给了我们什么。
甚至我们可以结合前面一章的not和when、identity函数来重构一下:
