iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 11
0
自我挑戰組

Good Morning, JS functional Programing.系列 第 11

Good Morning, Functional JS (Day 10, Partial Application 偏函數應用)

本系列文章,內容以探討 Kyle Simpson. Functional-Light JavaScript 一書內容為主

  • 目標:是讀懂 FP,能用 code 與人交流,而不是被壓在 FP 的術語大山下喘不過氣。
  • 原文地址:Functional-Light JavaScript

一些現在傳,一些等一下再傳

Partial Application,偏函數應用

如果一個函數接受多個參數,你可以會先傳入部分參數,剩下的稍後傳入,(待全部參數確定後再執行)。

模擬一個場合,要發一個 API 請求,但資料和 callback handler 要稍後才知道(可能在等待上一個 API 請求)。

function ajax( url, data, cb ) {

}

創建一個函數,手動設置 ajax 第一個參數,並等待另外兩個參數 data 和 callback

function getOrder(data, cb) {
    ajax( "http://some.api/order", data, cb );
}

當然可以再進一步手動做這樣的操作,

function getLastOrder(cb) {
    getOrder( { id: ORDER_ID }, cb );
}

相信讀者應該看出模式了,身為一個工程師應該很習慣在重複的行為中,找到轉換為邏輯的方法,上面三段手動修改只是為了演示,現在來檢查其中的概念。

Partial Application

請原諒我直接用術語來說明: getOrder(data,cb)ajax 的偏函數 partial applicationgetOrder 套用 (apply) 了實際參數 "http://some.api/order" 到形式參數 url 上,

換個說法,原先的 ajax 定義了三個形式參數 (parameter),假設我已知 url ,我先套用 "http://some.api/order" 到 url,先套用部分參數,接下來只要呼叫剩下兩個參數的 getOrder(data,cb) 版本即可,

還是很混亂?來段英文吧

翻譯:偏函數 partial application 正式來說是一種減少函數參數個數 Arity 的流程,Arity 指的是形式參數 parameter 的個數,如原先 ajax 的 arity 從 3 個減少到 2 個。

有了正式定義,就來實現一個 partial(..) 工具函數

function partial(fn, ...presetArgs) {
    return function partiallyApplied(...laterArgs) {
        return fn( ...presetArgs, ...laterArgs )
    }
}

// ES6, =>

var partial =
    (fn, ...presetArgs) =>
        (...laterArgs) =>
            fn( ...presetArgs, ...laterArgs )

建議:只是掃過看看是不行的喔!這個小工具的概念會一直出現在這系列文,請務必熟悉。

  • fn: 將被 partially apply 偏函數應用的函數,就是要被縮減參數個數
  • presetArgs: ... 收集部分先傳入的參數,保存到 presetArgs 陣列做稍後使用。

為什麼可以稍後使用呢?為什麼在 partial(..) 結束後,內部函數為何還能引用 fnpresetArgs? 就是 closure 哦!!,在內部呢,創建並 return 一個函數 partiallyAppliedpartiallyApplied 閉包 closes overfnpresetArgs

所以,稍後 partiallyApplied 在程式別處執行時,它其實是使用了傳入 partial 然後被 close over 的 fn 運行原函數,而參數呢?先是 presetArgs : 一開始作為 partial application 先傳入的參數(現已被 close over),接著是近一步傳入的 laterArgs

建議:困惑嗎?正常的,可以的話參考原文^2,接下來要更深入了。

重構

接著,來使用我們的 FP 小工具 partial 來重構一開始的三段,

var getOrder = partial(ajax, "http://some.api/order")

記得有一個 return 嗎,現在試想 getOrder 的內部的樣子

var getOrder = function partiallyApplied (...laterArgs) {
    return ajax("http://some.api/order", ...laterArgs)
}

那 getLastOrder 如何重構呢?

// ver1
var getLastOrder = partial(
    ajax,
    "http://some.api/order",
    { id: ORDER_ID }
)

// ver2
var getLastOrder = partial( getOrder, { id: ORDER_ID })
  • ver1: 直接指定 url 與 data 的實際參數來定義
  • ver2: 將 getLastOrder 定義成 getOrder 的 partial application,並指定 data。

ver2 重用了已定義好的 getOrder,所以表達上更清楚,也比較符合 FP 的精神。

再看看以下這段 code ,試著全盤理解 ver1 和 ver2 的內部運作

// ver1
var getLastOrder = function partiallyApplied (...laterArgs){
    return ajax(
        "http://some.api/order",
        { id: ORDER_ID }
    )
}
// ver2
var getLastOrder = function outerPartiallyApplied (...outerLaterArgs) {
    var getOrder = function innerPartiallyApplied (...innerlaterArgs) {
        return ajax("http://some.api/order", ...innerlaterArgs)
    }
    return getOrder({{ id: ORDER_ID }}, ...outerLaterArgs)
}

建議:為何要這樣一層一層包裝,哈!這就是 FP,請務必熟悉與習慣。

再來個 Partial Application: 範例

function add (x, y) {
    return x + y
}

// ES6, =>

var add = (x, y) => x + y

現在我們有一個成績陣列,因為題目出錯全體加十分,使用的是 map(...)

[30, 55, 42, 87, 66].map( ... )

// [40, 65, 52, 97, 76]

因為 add(x, y) 的函數簽名(signature),不是 map 預期,不能直接傳入 map(...),這時候我們就可以利用 partial application 幫助調整:

[30, 55, 42, 87, 66].map( partial( add, 10 ))

參數反序

在思考一個場合,如果 ajax( url, data, cb ) 現在要先傳 cb 稍後再 傳 dataurlpartially apply,現在來做一個 reverseArgs) FP 小工具:

function reverseArgs(fn) {
    return function argsReversed(...args){
        return fn( ...args.reverse() );
    };
}

// ES6 =>
var reverseArgs =
    fn =>
        (...args) =>
            fn( ...args.reverse() );

現在不再從左邊開始,而是從右邊開始偏應用 partially apply,另外,為了在 call function 的時候仍然使用原順序,所以在偏應用處理後,又在反序一次:

var getOrderWithHandler = reverseArgs(
    partial(reverseArgs( ajax ), function handler() {
        // ...
    })
)

好吧!來試著解釋這一段吧,FP 洋蔥語法從內層開始剝,(僅示意)

  1. reverseArgs( ajax ) 將參數反序並 return (cb, data, url)

  2. partial(...) 將 handler 套用在 (cb, data, url) 第一個位置 cb 並 return (data, url)

  3. 也就是 cb 已被 closes over

  4. 最後再次使用 reverseArgs , return (url, data)

  5. 故處理後,可以如同原本使用方式,

為了不手動做 reverseArgs 兩次,試著做另外一個 FP 小工具 partialRight

PartialRight

function partialRight(fn, ...presetArgs) {
    return reverseArgs(
        partial( reverseArgs( fn ), ...presetArgs.reverse() )
    );
}

如果你還是覺得兩次 reverseArgs 很冗餘,試試這樣

function partialRight(fn,...presetArgs) {
    return function partiallyApplied(...laterArgs){
        return fn( ...laterArgs, ...presetArgs );
    };
}

// ES6 =>
var partialRight =
    (fn,...presetArgs) =>
        (...laterArgs) =>
            fn( ...laterArgs, ...presetArgs );

不管是哪一個 partialRight,都不能保證讓一個特定形式參數 parameter 被偏應用 partially applied,只能確保傳入的值是最右邊的實際參數:

function some(x,y,z,...rest) {
    console.log( x, y, z, rest );
}

var f = partialRight( foo, "在你右邊" );

f(  );              // "在你右邊" undefined undefined []

f( 1, 2 );          // 1 2 "在你右邊" []

f( 1 );             // 1 "在你右邊" undefined []

f( 1, 2, 3 );       // 1 2 3 ["在你右邊"]

f( 1, 2, 3, 4 );    // 1 2 3 [4,"在你右邊"]

小結:

終於開始好玩的東西了,今天講的主題 Partial Application,偏函數應用,或者叫部份套用函式 Partially applied function,都是屬於翻譯差異,建議讀者有興趣一定要去翻翻相關資料,下個結論:對於某個函數,會先傳入部分實際參數,剩下的稍後傳入,稱之。

參考資料

[^1]: Partial application
[^2]: Some Now, Some Later


上一篇
Good Morning, JS (Day 9, ES6 Class 地雷)
下一篇
Good Morning, Functional JS (Day 11, 再探 Currying 柯里化)
系列文
Good Morning, JS functional Programing.31

尚未有邦友留言

立即登入留言