本系列文章,內容以探討 Kyle Simpson. Functional-Light JavaScript 一書內容為主
- 目標:是讀懂 FP,能用 code 與人交流,而不是被壓在 FP 的術語大山下喘不過氣。
 - 原文地址:Functional-Light JavaScript
 
Point-free(又寫成 Pointfree,中文:無參數,無點),正式名稱為:tacit programming,其中的 point(點)指的就是函數的 parameter(形式參數)。
寫在前面,Pointfree 透過隱藏 parameter - argument (形參 - 實參對應),減少視覺上的干擾,上層操作不直接操作數據,只合成運算過程,不過先提醒讀者,並非任何情況都適合 Pointfree,在本系列一直提到的:請權衡可讀性。
function double(x) {
    return x * 2
}
[1,2,3,4,5].map( function mapper( v ){
    return double( v );
} )
// [2,4,6,8,10]
範例1 一般寫法(含有 Point 點)
double 與 mapper 有相同的函數簽名 (signature) ,mapper形參(v) 可以直接對應到 double 實參(v),因此我們可以去掉 mapper 包裝,改寫成 Pointfree:
[1,2,3,4,5].map( double )
回顧 Day 7
["1","2","3"].map( function mapper(v){
    return parseInt( v );
} );
範例2
mapper 使得僅有一個參數能通過,排除 map(..) 傳入的 index ,避免 parseInt 錯把 index 當 radix 進行整數解析,還記得我們使用 unary 小工具進行處理如下:
// 小工具
// var unary = (fn) => (arg) => fn( arg )
["1","2","3"].map( unary( parseInt ) )
範例2' Pointfree
邏輯:我們藉由 unary 小工具,提取第一個形式參數,對映到 parseInt 的第一個實參,再如同範例一改寫成 Pointfree 風格,將不同簽名的map(..) 與 parseInt 組合。
還記得 Day 10 的 partialRight 嗎?或許你想這樣使用
map(partialRight(parseInt,10)) 
將 10 right-partially apply (右偏應用) 在 parseInt 的 radix,但請注意 map(..) 本身會傳 3 個參數 value, index, array,所以 10 會被當成第四個參數傳入 parseInt,partialRight 相關討論請看 Day 10。
FP 的目的就是透過組合基本的函數,完成複雜的工作,記得 樂高積木 的概念嗎,而 Pointfree 風格就是組合函數最好的體現。在看另一個例子:
定義一些基本函數:
// 輸出
function output(txt) {
    console.log( txt );
}
// 字串長度是否小於等於 5
function isShortEnough(str) {
    return str.length <= 5;
}
// 字串長度是否大於 5
function isLongEnough (str) {
    return !isShortEnough(str)
}
首先印出長度小於 5 的字串
function printIf( testfn, msg ) {
    if (testfn( msg )) {
        output( msg )
    }
}
var msg1 = "Good";
var msg2 = msg1 + " Morning";
printIf( isShortEnough, msg1 )            // Good
printIf( isShortEnough, msg2 )
印出長度大於 5 的字串
printIf( isLongEnough, msg1 )
printIf( isLongEnough, msg2 )       // Good Morning
看到 isLongEnough 的點了嗎? 形參 str 傳遞令你困擾,試著改成 Pointfree 吧!
介紹一個小工具 not(..) 取否定,在 FP libraries 中常稱為 complement(..) :
function not (testerfn) {
    return function negated (...args) {
        return !testerfn(...args)
    }
} 
// ES6
var not = testerfn =>
    (...args) =>
        !testerfn(...args)
FP 小工具 not(..)^1
使用 not(..) 改寫 isLongEnough:
var isLongEnough = not(isShortEnough)
printIf( isLongEnough, msg2 )       // Good Morning
範例3 isLongEnough Pointfree 版本
再介紹一個小工具 when(..) 改寫 if 條件判斷,
function when (testerfn, fn) {
    return function conditional (...args) {
        if (testerfn(...args)) {
            return fn(...args)
        }
    }
}
// ES6
var when = (testerfn, fn) =>
    (...args) =>
        testerfn(...args) ? fn(...args) : undefined
FP 小工具 when(..)^2
console.log(f2)
console.log(f2(isShortEnough))
console.log(f2(isShortEnough)('Morning'))
步驟1:
首先將 output right-partially apply (右偏應用) 在 when 的 fn,得到一個期望接受第一個參數 testerfn 的函數。
rightPartial( when, output )
// (...laterArgs) => when ( ...laterArgs, output)
步驟2:
將判斷 testerfn 傳入後,會產生另外一個等待參數 ...args (如下說明碼,也就是要輸出的字串) 的函數,
如果這樣印出來,就會比較清楚了

步驟3:
經過上面兩步驟的處理,函數簽名會變成 fn(testerfn)(str),想要調整成跟原來 printIf(testerfn, str) 相同,可以使用 uncurry(..) 註:關於 uncurry,請參考原文:
var printIf = uncurry( rightPartial( when, output ) );
範例4 Pointfree 的 printIf
這 5 天花了很多力氣深入探討函數參數的調整方式,也開始接觸函數的組成,小整理如下:
Partial Application,偏函數應用,返回一個預設部分參數的函數,等待剩下全部的參數。
Currying,是 Partial 的特殊形式,將參數數量減少到 1 個,每一次傳入參數都會返回一個另一個等待接受下一個參數的函數,直到收到所有參數,再執行。
還有介紹許多小工具(整理如下),包含解構,反序、反 curry...等,都是 FP 函式庫常用的工具。
Pointfree 無點風格,透過隱藏 parameter - argument (形參 - 實參對應),目的在於提高程式碼的可讀性。
^1, ^2: No Points
^3: FP 小工具整理,截至Day 14 (2018-01-03)
All for one, unary
var unary = fn => arg => fn( arg )
One on one, identity
var identity = v => v
Unchanging One, constant (for certain APIs)
var constant = v => () => v
SpreadArgs, apply(..)
var spreadArgs = fn => argsArr => fn( ...argsArr )
GatherArgs, unapply(..)
var gatherArgs = fn => ...argArr => fn(argArr)
Partial Application, partial(..)
var partial = 
    (fn, ...presetArgs) =>
        (...laterArgs) =>
            fn(...presetArgs, ...laterArgs)
Partial Application, partialRight(..)
var partialRight =
    (fn, ...presetArgs) =>
        (...laterArgs) => 
            fn(...laterArgs, ...presetArgs)
Currying, curry(..)
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)
            }    
        }
    )( [] )
No Currying, uncurry(..)
var uncurry =
    fn =>
        (...args) => {
            var ret = fn;
            for (let arg of args) {
                ret = ret( arg );
            }
            return ret;
        };
重構 partial application 工具 (參數解構, 命名實參)
function partialProps(fn,presetArgsObj) {
    return function partiallyApplied(laterArgsObj){
        return fn( Object.assign( {}, presetArgsObj, laterArgsObj ) );
    };
}
重構 currying 工具 (參數解構, 命名實參)
// 請注意,arity 需指定,預設為 1
function curryProps(fn, arity = 1) {
    return (function nextCurried(prevArgsObj){
        return function curried(nextArgObj = {}){
            var [key] = Object.keys( nextArgObj );
            var allArgsObj = Object.assign( {}, prevArgsObj, { [key]: nextArgObj[key] } );
            if (Object.keys( allArgsObj ).length >= arity) {
                return fn( allArgsObj );
            }
            else {
                return nextCurried( allArgsObj );
            }
        };
    })( {} );
}
spreadArgProps(..) 解析並解構形參
function spreadArgProps(
    fn,
    propOrder =
        fn.toString()
        .replace( /^(?:(?:function.*\(([^]*?)\))|(?:([^\(\)]+?)\s*=>)|(?:\(([^]*?)\)\s*=>))[^]+$/, "$1$2$3" )
        .split( /\s*,\s*/ )
        .map( v => v.replace( /[=\s].*$/, "" ) )
) {
    return function spreadFn(argsObj) {
        return fn( ...propOrder.map( k => argsObj[k] ) );
    };
}
not(..),取反 (取補集),
complement(..)
var not = testerfn => (...args) => !testerfn(...args)
when(..),重構 if
var when = (testerfn, fn) =>
    (...args) =>
        testerfn(...args) ? fn(...args) : undefined