到了第四章了,函数式编程的魅力似乎越来越大。
对于函数式编程者,他们会将每个函数都当成是一个“部件”,在需要时通过组装不同的“部件”,来拼凑出一个自己想要的“模型”。
专业点的角度来说,就是我们能够定义某种组合方式,来让它们成为一种新的组合函数,程序中不同的部分都可以使用这个函数。这种将函数一起使用的过程叫做组合。
再介绍组合函数的概念之前,我们就已经使用过组合了。
例如在之前我们的一个案例:
unary(adder(3))
上面的表达式,我们将两个函数整合起来,然后将第一个函数调用产生的值(输出)当成第二个函数调用的实参(输入)。画个简图,也就是这样:
functionValue <-- unary <-- adder <-- 3
3
是 adder(..)
的输入。而 adder(..)
的输出是 unary(..)
的输入。unary(..)
的输出是 functionValue
。 这就是 unary(..)
和 adder(..)
的组合。
为了满足上面组合函数的要求,我们可以来构造这么一个简单的函数:
function compose2(fn2,fn1) {
return function composed(origValue){
return fn2( fn1( origValue ) );
};
}
// ES6 箭头函数形式写法
var compose2 =
(fn2,fn1) =>
origValue =>
fn2( fn1( origValue ) );
它能够自动创建两个函数的组合,这和我们手动做的是一模一样的。
现在有这么一个需求,需要将给定的一个英文字符串,提取其中全部的英文单词,先全部转化小写,然后去除其中重复的单词。
我们可以先来创建这么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
在上面我们构造了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
函数来重构一下:
// 取反辅助函数
function not(predicate) {
return function negated(...args) {
return !predicate(...args)
}
}
// 判断某个条件成立之后执行fn
function when(predicate, fn) {
return function conditional(...args) {
if (predicate(...args)) {
return fn(...args)
}
}
}
// 传一个返回一个
function identity(v) {
return v;
}
var isLong = (str) => str.length > 4;
var isShort = not(isLong)
var returnLong = when(isLong, identity)
var returnShort = when(isShort, identity)
function skipShortWords(list) {
return list.filter(str => returnLong(str))
}
function skipLongWords(list) {
return list.filter(str => returnShort(str))
}
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"]
function compose(...fns) {
return fns.reverse().reduce( function reducer(fn1,fn2){
return function composed(...args){
return fn2( fn1( ...args ) );
};
} );
}
// ES6 箭头函数形式写法
var compose =
(...fns) =>
fns.reverse().reduce( (fn1,fn2) =>
(...args) =>
fn2( fn1( ...args ) )
);
function compose(...fns) {
// 拿出最后两个参数
var [ fn1, fn2, ...rest ] = fns.reverse();
var composedFn = function composed(...args){
return fn2( fn1( ...args ) );
};
if (rest.length == 0) return composedFn;
return compose( ...rest.reverse(), composedFn );
}
// ES6 箭头函数形式写法
var compose =
(...fns) => {
// 拿出最后两个参数
var [ fn1, fn2, ...rest ] = fns.reverse();
var composedFn =
(...args) =>
fn2( fn1( ...args ) );
if (rest.length == 0) return composedFn;
return compose( ...rest.reverse(), composedFn );
};
我们早期谈及的是从右往左顺序的标准 compose(..)
实现。这么做的好处是能够和手工组合列出参数(函数)的顺序保持一致。
不足之处就是它们排列的顺序和它们执行的顺序是相反的,这将会造成困扰。同时,不得不使用 partialRight(compose, ..)
提早定义要在组合过程中 第一个 执行的函数。
相反的顺序,从右往左的组合,有个常见的名字:pipe(..)
。
pipe(..)
与 compose(..)
一模一样,除了它将列表中的函数从左往右处理。
function pipe(...fns) {
return function piped(result){
var list = fns.slice();
while (list.length > 0) {
// 从列表中取第一个函数并执行
result = list.shift()( result );
}
return result;
};
}
实际上,我们只需将 compose(..)
的参数反转就能定义出来一个 pipe(..)
。
var pipe = reverseArgs( compose );
回忆下之前的通用组合的例子:
var biggerWords = compose( skipShortWords, unique, words );
以 pipe(..)
的方式来实现,我们只需要反转参数的顺序:
var biggerWords = pipe( words, unique, skipShortWords );
pipe(..)
的优势在于它以函数执行的顺序排列参数,某些情况下能够减轻阅读者的疑惑
先来介绍2个简单且实用的函数
将任意对象的任意属性通过属性名提取出来。让我们把这个实用函数称为 prop(..)
:
function prop(name,obj) {
return obj[name];
}
// ES6 箭头函数形式
var prop =
(name,obj) =>
obj[name];
使用:
var obj = { x: 1, y: 2 }
prop('x', obj)
// 1
我们处理对象属性的时候,也需要定义下反操作的工具函数:setProp(..)
,为了将属性值设到某个对象上。
function setProp(name,obj,val) {
var o = Object.assign( {}, obj );
o[name] = val;
return o;
}
使用:
var obj = { x: 1, y: 2 }
var obj2 = setProp('z', obj, 3)
// { x: 1, y: 2, z: 3 }
function makeObjProp(name,value) {
return setProp( name, {}, value );
}
// ES6 箭头函数形式
var makeObjProp =
(name,value) =>
setProp( name, {}, value );
提示: 这个实用函数在 Ramda 库中被称为 objOf(..)
。
让我们回顾一下第二章介绍的ajax
案例
function ajax (url, data, callback) {
// ...
}
var getUser = partial( ajax, "/api/user" );
var getLastOrder = partial( ajax, "/api/order", { orderId: -1 } );
var output = (str) => console.log(sgr);
getLastOrder(function orderFound(order) {
getUser({ userId: order.userId }, function userFound (user) {
output(user.name)
})
})
如上,我们在给getLastOrder
函数传递最后一个参数(一个回调函数orderFound
)
该函数用查询到的订单信息order
中的userId
查询当前订单的用户,并输出用户的姓名name
.
可以看到上面的函数需要order
和user
两个形参。
我们可以用现有的函数式编程的知识将其转化为一个无形参的函数getLastOrder
.
从里向外,我们先想想如何移除user
这个形参。
首先output
函数是需要接收user.name
这个参数的。我们可以用什么样的方式来移除这个参数呢。
在这里我们的目的是想要获取user
中的name
属性:
定义一个extractName
函数:
var extractName = partial(prop, 'name')
之后我们就可以直接用:
extractName(user)
这样的方式获取到user.name
接着你是不是也想到可以用compose
了呢?
var outputUserName = compose( output, extractName )
想一下我们需要的数据流是什么样:
output <-- extractName <-- user
下一步,让我们缩小关注点,看下例子中嵌套的这块查找操作的调用:
getLastOrder( function orderFound(order){
getUser( { userId: order.userId}, outputUserName );
} );
我们刚刚创建的 outputUserName(..)
函数是提供给 getUser(..)
的回调。所以我们还能定义一个函数叫做 processUser(..)
来处理回调参数,使用 partialRight(..)
:
var processUser = partialRight( getUser, outputUserName )
让我们用新函数来重构下之前的代码:
getLastOrder(function orderFound(order) {
processUser({ userId: order.userId })
})
Ok,至此,user
这个形参已经被我们干掉了。
接下来你可以用类似的方式移除掉order
形参。
首先是获取userId
var extractUserId = partial(prop, 'userId')
接着你需要定义一个函数来解决{ userId: order.userId }
这个问题。我们可以使用上面的makeObjProp
函数:
var userData = partial(makeObjProp, 'userId')
为了使用 processUser(..)
来完成通过 order
值查找一个人的功能,我们需要的数据流如下:
processUser <-- userData <-- extractUserId <-- user
所以我们只需要再使用一次 compose(..)
来定义一个 lookupUser(..)
:
var lookupUser = compose( processUser, userData, extractUserId )
然后,就是这样了!把这整个例子重新组合起来,不带任何的“形参”:
function ajax (url, data, callback) {
// ...
};
var getUser = partial( ajax, "/api/user" );
var getLastOrder = partial( ajax, "/api/order", { orderId: -1 } );
var output = (str) => console.log(sgr);
var extractName = partial(prop, 'name');
var outputUserName = compose( output, extractName );
var processUser = partialRight( getUser, outputUserName )
var extractUserId = partial(prop, 'userId')
var userData = partial(makeObjProp, 'userId')
var lookupUser = compose( processUser, userData, extractUserId )
getLastOrder( lookupUser )
Look!不带任何的形参。
函数组合是一种定义函数的模式,它能将一个函数调用的输出路由到另一个函数的调用上,然后一直进行下去。
因为 JS 函数只能返回单个值,这个模式本质上要求所有组合中的函数(可能第一个调用的函数除外)是一元的,当前函数从上一个函数输出中只接收一个输入。
相较于在我们的代码里详细列出每个调用,函数组合使用
compose(..)
实用函数来提取出实现细节,让代码变得更可读,让我们更关注组合完成的是什么,而不是它具体做什么。组合 ———— 声明式数据流 ———— 是支撑函数式编程其他特性的最重要的工具之一。