本系列文章,內容以探討 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 的議題。