JS輕量級函數式編程實現XDM二

前言

承接上一篇《XDM,JS如何函數式編程?看這就夠瞭!(一)》,我們知道瞭函數式編程的幾個基本概念。

這裡作簡要回顧:

  • 函數式編程目的是為瞭數據流更加明顯,從而代碼更具可讀性;
  • 函數需要一個或多個輸入(理想情況下隻需一個!)和一個輸出,輸入輸出是顯式的代碼將更好閱讀;
  • 閉包是高階函數的基礎;
  • 警惕匿名函數;
  • 棄用 this 指向;

本篇將著重介紹第 2 點中函數的輸入,它是 JS 輕量函數式編程的基礎之基礎,重要之重要!!!

偏函數

傳參現狀

我們經常會寫出這樣的代碼:

function ajax(url,data,callback) {
    // ..
}
function getPerson(data,cb) {
    ajax( "http://some.api/person", data, cb );
}

ajax 函數有三個入參,在 getPerson 函數裡調用,其中 url 已確定,data 和 cb 兩個參數則等待傳入。(因為很多時候參數都不是在當前能確定的,需要等待其它函數的操作後確定瞭再繼續傳入)

但是我們的原則是:入參最理想的情況下隻需一個!

怎樣優化,可以實現這一點呢?

我們或許可以在外層再套一個函數來進一步確定傳參,比如:

function getCurrentUser(cb) {
    ...// 通過某些操作拿到 CURRENT_USER_ID
    getPerson( { user: CURRENT_USER_ID }, cb );
}

這樣,data 參數也已經確定,cb 參數仍等待傳入;函數 getCurrentUser 就隻有一個入參瞭!

數據的傳遞路線是:

ajax(url,data,callback) => getPerson(data,cb) => getCurrentUser(cb)

這樣函數參數個數逐漸減少的過程就是偏應用。

也可以說:getCurrentUser(cb) 是 getOrder(data,cb) 的偏函數,getOrder(data,cb) 是 ajax(url,data,cb) 函數的偏函數。

設想下:

如果一個函數是這樣的:

function receiveMultiParam(a,b,c,......,x,y,z){
    // ..
}

我們難道還要像上面那樣手動指定外層函數進行逐層嵌套嗎?

顯示我們不會這麼做!

封裝 partial

我們隻需要封裝一個 partial(..) 函數:

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

它的基礎邏輯是:

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

把函數作為入參!還記得我們之前所說:

一個函數如果可以接受或返回一個甚至多個函數,它被叫做高階函數。

我們借用 partial() 來實現上述舉例:

var getPerson = partial( ajax, "http://some.api/person" );
var getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } ); // 版本 1

以下函數內部分析非常重要:

運行機制

getPerson() 的內部運行機制是:

var getPerson = function partiallyApplied(...laterArgs) {
    return ajax( "http://some.api/person", ...laterArgs );
};

getCurrentUser() 的內部運行機制是:

var getCurrentUser = function outerPartiallyApplied(...outerLaterArgs) {
    var getPerson = function innerPartiallyApplied(...innerLaterArgs){
        return ajax( "http://some.api/person", ...innerLaterArgs );
    };
    return getPerson( { user: CURRENT_USER_ID }, ...outerLaterArgs );
}

數據進行瞭傳遞:

getCurrentUser(outerLaterArgs) => getPerson(innerLaterArgs) => ajax(...params)

我們通過這樣一層額外的函數包裝層,實現瞭更加強大的數據傳遞,

我們將需要減少參數輸入的函數傳入 partial()中作為第一個參數,剩下的是 presetArgs,當前已知幾個,就可以寫幾個。還有不確定的入參 laterArgs,可以在確定後繼續追加。

像這樣進行額外的高階函數包裝層,是函數式編程的精髓所在!

“隨著本系列的繼續深入,我們將會把許多函數互相包裝起來。記住,這就是函數式編程!” —— 《JavaScript 輕量級函數式編程》

實際上,實現 getCurrentUser() 還可以這樣寫:

// 版本 2
var getCurrentUser = partial(
    ajax,
    "http://some.api/person",
    { user: CURRENT_USER_ID }
);
// 內部實現機制
var getCurrentUser = function partiallyApplied(...laterArgs) {
    return ajax(
        "http://some.api/person",
        { user: CURRENT_USER_ID },
        ...laterArgs
    );
};

但是版本 1 因為重用瞭已經定義好的函數,所以它在表達上更清晰一些。它被認為更加貼合函數式編程精神!

拓展 partial

我們再看看 partial() 函數還可它用:

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

比如:將數組 [1,2,3,4,5] 每項都加 3,通常我們會這麼做:

function add(x,y) {
    return x + y
[1,2,3,4,5].map( function adder(val){
    return add( 3, val );
} );
// [4,5,6,7,8]

借助 partial():

[1,2,3,4,5].map( partial( add, 3 ) );
// [4,5,6,7,8]

add(..) 不能直接傳入 map(..) 函數裡,通過偏應用進行處理後則能傳入;

實際上,partial() 函數還可以有很多變體:

回想我們之前調用 Ajax 函數的方式:ajax( url, data, cb )。如果要偏應用 cb 而稍後再指定 data 和 url 參數,我們應該怎麼做呢?

function reverseArgs(fn) {
    return function argsReversed(...args){
        return fn( ...args.reverse() );
    };
}
function partialRight( fn, ...presetArgs ) {
    return reverseArgs(
        partial( reverseArgs( fn ), ...presetArgs.reverse() )
    );
}
var cacheResult = partialRight( ajax, function onResult(obj){
    cache[obj.id] = obj;
});
// 處理後:
cacheResult( "http://some.api/person", { user: CURRENT_USER_ID } );

柯裡化

函數柯裡化實際上是一種特殊的偏函數。

我們用 curry(..) 函數來實現此前的 ajax(..) 例子,它會是這樣的:

var curriedAjax = curry( ajax );
var personFetcher = curriedAjax( "http://some.api/person" );
var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } );
getCurrentUser( function foundUser(user){ /* .. */ } );

柯裡化函數:接收單一實參(實參個數:1)並返回另一個接收下一個實參的函數。

它將一個函數從可調用的 f(a, b, c) 轉換為可調用的 f(a)(b)(c)。

實現:

function curry(fn,arity = fn.length) {
    return (function nextCurried(prevArgs){
        return function curried(nextArg){
            var args = prevArgs.concat( [nextArg] );
            if (args.length >= arity) {
                return fn( ...args );
            }
            else {
                return nextCurried( args );
            }
        };
    })( [] );
}

階段小結

我們為什麼要如此著重去談“偏函數”(partial(sum,1,2)(3))或“柯裡化”(sum(1)(2)(3))呢?

第一,是顯而易見的,偏函數或柯裡化,可以將“指定分離實參”的時機和地方獨立開來;

第二,更有重要意義的是,當函數隻有一個形參時,我們能夠比較容易地組合它們。這種單元函數,便於進行後續的組合函數;

對函數進行包裝,使其成為一個高階函數是函數式編程的精髓!

至此,有瞭“偏函數”這門武器大炮,我們將逐漸轟開 JS輕量級函數式編程的面紗 ~

以上就是JS輕量級函數式編程實現XDM二的詳細內容,更多關於JS輕量級函數式編程XDM的資料請關註WalkonNet其它相關文章!

推薦閱讀: