本系列文章,內容以探討 Kyle Simpson. Functional-Light JavaScript 一書內容為主
- 目標:是讀懂 FP,能用 code 與人交流,而不是被壓在 FP 的術語大山下喘不過氣。
 - 提醒:本文中各種的 FP 小工具,僅為邏輯演示,實際上並不適合在 production 中使用,建議使用 FP library。
 - 原文地址:Functional-Light JavaScript
 
既然可以合成兩個函數,當然也可以合成多個:
output <-- fn1 <-- fn2 <-- ... <-- fnN <--input
定義小工具 compose(..) 如下:
function compose(...fns) {
    return function composed(result) {
        // 複製 fns 陣列到 list
        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
    }
小工具 compose(..)
延伸 Day15 的範例,使用到的基本運算如下
function splitString(str) {
    return String( str )
        .toLowerCase()
        .split( /\s|\b/ )
        .filter( function alpha(v){
            return /^[\w]+$/.test( v );
        } );
}
function deDuplicate(list) {
    return Array.from(new Set(list));
}
function skipShortWords(words) {    
    return words.filter(word => word.length > 4)
}
code 字串分解、去除陣列重複元素、排除短字串 ^源碼連結
現在要從一串字串中找出長度大於4的單字,手動合成這些函數:
var result = skipShortWords( deDuplicate( splitString(str) ) )
使用 compose(..) 自動合成:
var longWords = compose(skipShortWords, deDuplicate, splitString)
var result = longWords(str)
範例1 找長單字 compose(..)應用,^JSBIN 練習
現在試著應用 partialRight 的概念,實作一個右偏應用的 compose (也就是預先定義 splitString, deDuplicate),稱為 wordsFilter(..):
var partialRight =
    (fn,...presetArgs) =>
        (...laterArgs) =>
            fn( ...laterArgs, ...presetArgs );
關於 partialRight,見 Day 10 討論
var wordsFilterBy = partialRight(compose, deDuplicate, splitString)
var longWords = wordsFilterBy( skipShortWords )
範例2 找長單字 partialRight compose 應用
如果在第二步驟,使用 skipLongWords:
var shortWords = wordsFilterBy( skipLongWords )
範例3 找短單字 partialRight compose 應用
從這邊可以看到在第二步使用不同的 filter (skipShortWords 和 skipLongWords) 創造出功能不同的函數,這就是 函數合成 Composition - FP 最強大的地方。
當然也可以使用 currying 處理,
var curry = (fn, ARITY = fn.length, nextCurried) =>
    (nextCurried = prevArgs =>
        nextArg => {
            var args = [...prevArgs, nextArg]
            if (args.length >= ARITY) {
                return fn(...args)
            } else {
                return nextCurried(args)
            }    
        }
    )( [] )
關於 currying,見 Day 11 討論
因為 compose 參數由右到左處理,如果是使用我們自製的 currying,可能需要使用 reverseArgs 操作,並且指定參數數目為 3,因為 compose 是可變參數函數,寫法如下:
var curried_compose = curry(reverseArgs(compose), 3)
curried_compose(splitString)(deDuplicate)(skipShortWords)
範例4 找長單字 curry compose 應用,^JSBIN 練習
試著使用 reduce(..) 重構 compose(..) :
function compose(...fns) {
    return function composed(result) {
        return fns
        .reverse()
        .reduce(function reducer( result, fn ) => {
            return fn(result)
        }, result )
    }
}
// ES6
var compose = (...fns) =>
    result => 
        fns
        .reverse()
        .reduce((result, fn) => fn(result), result)        
    
小工具 compose(..),reduce(..)版本
到目前為止,我們組合的每一個函數都是單參數函數,如果要傳多實參給第一層,可以加一層 lazy-evaluation function 包裝:
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 ) )
        );
小工具 處理多參數輸入的 compose
function composed(...args){
            return fnN(...fn2( ( fn1( ...args ) )...)
}
今天整理了更多 compose 的方式,以及回顧了 partial 和 curry,結合各種函數的 FP 真的越來越有趣了,明天將繼續探討 compose 的議題。