第2章 函數(shù)組合

組合函數(shù)就像堆樂高積木一樣凰慈,將不同的組件按照不同的方式拼成一個有用的組件士袄。

一.輸出到輸入

上一章我們已經(jīng)看到過一些組合函數(shù),比如 unary(adder(3)), 這將2個函數(shù)組合到一起腮敌,其數(shù)據(jù)流如下:

functionValue <-- unary <-- adder <-- 3

3adder(..) 的輸入值竖瘾, 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末姻蚓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匣沼,更是在濱河造成了極大的恐慌狰挡,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件释涛,死亡現(xiàn)場離奇詭異加叁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)唇撬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門它匕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人窖认,你說我怎么就攤上這事豫柬。” “怎么了扑浸?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵烧给,是天一觀的道長。 經(jīng)常有香客問我喝噪,道長础嫡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任酝惧,我火速辦了婚禮驰吓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘系奉。我一直安慰自己,他們只是感情好姑廉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布缺亮。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪萌踱。 梳的紋絲不亂的頭發(fā)上葵礼,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機(jī)與錄音并鸵,去河邊找鬼鸳粉。 笑死,一個胖子當(dāng)著我的面吹牛园担,可吹牛的內(nèi)容都是我干的届谈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼弯汰,長吁一口氣:“原來是場噩夢啊……” “哼艰山!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起咏闪,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤曙搬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鸽嫂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纵装,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年据某,在試婚紗的時候發(fā)現(xiàn)自己被綠了橡娄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡哗脖,死狀恐怖瀑踢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情才避,我是刑警寧澤橱夭,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站桑逝,受9級特大地震影響棘劣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜楞遏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一茬暇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧寡喝,春花似錦糙俗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春劈彪,著一層夾襖步出監(jiān)牢的瞬間竣蹦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工沧奴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痘括,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓滔吠,卻偏偏與公主長得像纲菌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子屠凶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容