組合函數(shù)就像堆樂高積木一樣凰慈,將不同的組件按照不同的方式拼成一個有用的組件士袄。
一.輸出到輸入
上一章我們已經(jīng)看到過一些組合函數(shù),比如 unary(adder(3)), 這將2個函數(shù)組合到一起腮敌,其數(shù)據(jù)流如下:
functionValue <-- unary <-- adder <-- 3
3
是 adder(..)
的輸入值竖瘾, adder(..)
的輸出值 又是 unary(..)
的輸入值, unary(..)
的輸出值為 functionValue
, 這叫做 unary(..)
和 adder(..)
的組合组底。 這種組合就像傳送帶一樣丈积,比如巧克力的生產(chǎn): 冷卻 --> 切割 --> 包裝筐骇。
1.命令式函數(shù)示例
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) )
這樣變?yōu)橐粋€組合函數(shù)铛纬,注意傳統(tǒng)的函數(shù) left-to-right, 但是組合函數(shù)一般都是 right-to-left 或者 inner-to-outer.
我們可以將上面的函數(shù)進(jìn)一步的進(jìn)行封裝:
function uniqueWords(str) {
return unique( words(str) );
}
// 這樣就可以不再寫
var wordsUsed = unique( words(text) )
// 而是直接寫為
var wordsUsed = uniqueWords(text);
2.Machine Making
上面的例子就像我們巧克力廠設(shè)備組合在一起,去掉了中間過程(冷卻唬滑,切割告唆,包裝),直接輸入一個值就輸出一個值晶密。下面我們進(jìn)一步的"生產(chǎn)"一種設(shè)備擒悬,這個設(shè)備能夠直接制造出向uniqueWords
這樣的設(shè)備。
function compose2(fn2, fn1) {
return function composed(oriValue) {
return fn2( fn1(oriValue) );
}
};
// ES6寫法
const compose2 =
(fn2, fn1) =>
oriValue =>
fn2( fn1(oriValue) );
下面我們"生產(chǎn)" uniqueWords
這種設(shè)備:
var uniqueWords = compose2(unique, words);
var wordsUsed = uniqueWords(text);
3.組合變形
<-- unique <-- words
這種組合似乎是這兩個函數(shù)唯一組合順序稻艰,但其實(shí)我們可以像樂高積木一樣改變2個組件的組合方式來改變其功能懂牧。
var letters = compose2(words, unique);
var chars = letters("How are you Henry?");
chars;
// ["h","o","w","a","r","e","y","u","n"]
這個例子雖然比較巧合,但是這里是為了說明函數(shù)的組合不是一直是單向的
二.通用組合
如果我們可以將2個函數(shù)組合在一起连锯,我們可以將任意多個函數(shù)組合在一起归苍,通用數(shù)據(jù)流如下:
finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- oriValue
1.通用實(shí)現(xiàn)
function compose(...fns) {
return function composed(result) {
// 賦值上面的函數(shù)數(shù)組
var lists = fns.slice();
while (list.length > 0) {
// 將最后一個函數(shù)從數(shù)組中取出, pop()
// 并且執(zhí)行它
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))
的基礎(chǔ)上加一層過濾條件, 對單詞長度小于4的進(jìn)行過濾运怖,即 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.結(jié)合partialRight()
使用 partialRight() 我們可以做一些更有意思的事拼弃,比如先指定后面的2個參數(shù) unique(..)
和 words(..)
// skipShortWords中使用 "<= 4" 代替 "> 4"
// 改名為skipLongWords
function skipLongWords(list) { /* ... */}
// 先指定后面的2個參數(shù)
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后面的參數(shù)摇展,后續(xù)步驟(或邏輯)可以根據(jù)自己的需求進(jìn)行書寫吻氧。
同樣我們可以使用 curry
對組合函數(shù)進(jìn)行控制,比如:
// 一般我們先將compose的參數(shù)反過來咏连,這樣就可以
// fn1 --> fn2--> ...
curry( reverseArgs(compose), 3)(words)(unique)(..)
三.使用Reduce實(shí)現(xiàn)通用組合
我們可能永遠(yuǎn)不會在項(xiàng)目中自己實(shí)現(xiàn)compose(..), 用么使用一些工具庫幫助我們實(shí)現(xiàn)盯孙, 但是理解它的原理能夠有效的幫助我們更好的函數(shù)編程。
1.使用reduce
這種實(shí)現(xiàn)方式更加的簡潔祟滴,使用函數(shù)編程中常用的 reduce 函數(shù)
function compose(...fns) {
return function composed(result) {
// 此時fns.reverse()數(shù)組為
// [fn1, fn2, ..., fnn]
return fns.reverse().reduce(function reducer(result, fn) {
return fn(result);
}, result);
};
}
// reduce函數(shù)
// fn1(result) --> fn2( fn1(result) ) --> ...
// 這樣嵌套下去
// ES6
const compose =
(...fns) =>
result =>
fns.reverse().reduce(
(result, fn) => fn(result),
result
);
2.首次調(diào)用傳入多個參數(shù)的情況
上面的函數(shù)我們可以知道振惰,每次傳入的參數(shù)都只能為1個,這樣大大的限制了函數(shù)的能力垄懂,如果函數(shù)都是一元的這樣無所謂骑晶,但是對多遠(yuǎn)的就不適用。
function compose(...fns) {
// reduce寫在前面草慧, 等待所有函數(shù)全部傳入完畢后計(jì)算
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) );
);
這種方式桶蛔,先調(diào)用reduce,然后將所有的組合組合完成之后做一次性的計(jì)算;前面的compose都是傳入一個函數(shù)就計(jì)算出結(jié)果漫谷,然后將結(jié)果作為下一次的輸入值
3.使用遞歸來實(shí)現(xiàn)compose
遞歸形式為:
compose( compose(fn1, fn2, ..., fnN-1), fnN );
實(shí)現(xiàn)遞歸:
function compose(...fns) {
// 取出最后面的2個實(shí)參
// rest = [fn3, fn4, ..., fnN]
var [fn1, fn2, ...rest] = fns.reverse();
var composedFn = function composed(...args) {
return fn2( fn1(...args) );
};
// 如果只有2個函數(shù)組合
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);
};
遞歸的實(shí)現(xiàn)方式思路更加的清晰
四.改變實(shí)現(xiàn)順序 pipe()
上面的compose傳參的順序是 from-right-to-left, 這種順序的優(yōu)勢是和書寫順序一致仔雷,有另一種 from-left-to-right 的順序, 其本質(zhì)就是,就是使用 shift() 替換 pop()碟婆。使用pipe()的優(yōu)勢是电抚,按照參數(shù)執(zhí)行順序傳入。
function pipe(...fns) {
return function piped(result) {
var list = fns.slice();
while (list.length > 0) {
// 取出第一個參數(shù)并執(zhí)行
result = list.shift()(result);
}
return result;
}
}
// 或者直接對compose的傳參順序進(jìn)行反向
// 利用reverseArgs()
const pipe = reverseArgs(compose);
所以上面的一些例子可以寫為:
var biggerWords = compose( skipShortWords, unique, words);
// 使用pipe()
var biggerWords = pipe(words, unique, skipShortWords);
// 還有結(jié)合partialRight()
var filterWords = partialRight(compose, unique, words);
var filterWords = partialRight(pipe, words, unique);
總結(jié)
本章主要是談組合脑融,即將不同的函數(shù)結(jié)合成一個函數(shù)喻频,也介紹了通用組合的幾種實(shí)現(xiàn)方式。一種是只能傳入一元參數(shù)肘迎,另一種是首次調(diào)用不限制傳入?yún)?shù)的個數(shù)甥温,以及使用遞歸的方法實(shí)現(xiàn)compose.其中也利用了上一章中實(shí)現(xiàn)的 partialRight
, reverseArgs
的輔助函數(shù)。另外抽象和point-free的介紹將在后續(xù)的章節(jié)中補(bǔ)充妓布。
2016/10/29 14:59:35