摘要:但是,對函數(shù)式編程而言,這個(gè)行為的重要性是毋庸置疑的。關(guān)于該模式更正式的說法是偏函數(shù)嚴(yán)格來講是一個(gè)減少函數(shù)參數(shù)個(gè)數(shù)的過程這里的參數(shù)個(gè)數(shù)指的是希望傳入的形參的數(shù)量。
原文地址:Functional-Light-JS
原文作者:Kyle Simpson-《You-Dont-Know-JS》作者
第 3 章:管理函數(shù)的輸入(Inputs)關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;分享,是 CSS 里最閃耀的一瞥;總結(jié),是 JavaScript 中最嚴(yán)謹(jǐn)?shù)倪壿?。?jīng)過捶打磨練,成就了本書的中文版。本書包含了函數(shù)式編程之精髓,希望可以幫助大家在學(xué)習(xí)函數(shù)式編程的道路上走的更順暢。比心。
譯者團(tuán)隊(duì)(排名不分先后):阿希、blueken、brucecham、cfanlife、dail、kyoko-df、l3ve、lilins、LittlePineapple、MatildaJin、冬青、pobusama、Cherry、蘿卜、vavd317、vivaxy、萌萌、zhouyao
在第 2 章的 “函數(shù)輸入” 小節(jié)中,我們聊到了函數(shù)形參(parameters)和實(shí)參(arguments)的基本知識(shí),實(shí)際上還了解到一些能簡化其使用方式的語法技巧,比如 ... 操作符和解構(gòu)(destructuring)。
在那個(gè)討論中,我建議盡可能設(shè)計(jì)單一形參的函數(shù)。但實(shí)際上你不能每次都做到,而且也不能每次都掌控你的函數(shù)簽名(譯者注:JS 中,函數(shù)簽名一般包含函數(shù)名和形參等函數(shù)關(guān)鍵信息,例如 foo(a, b = 1, c))。
現(xiàn)在,我們把注意力放在更復(fù)雜、強(qiáng)大的模式上,以便討論處在這些場景下的函數(shù)輸入。
立即傳參和稍后傳參如果一個(gè)函數(shù)接收多個(gè)實(shí)參,你可能會(huì)想先指定部分實(shí)參,余下的稍后再指定。
來看這個(gè)函數(shù):
function ajax(url,data,callback) { // .. }
想象一個(gè)場景,你要發(fā)起多個(gè)已知 URL 的 API 請求,但這些請求的數(shù)據(jù)和處理響應(yīng)信息的回調(diào)函數(shù)要稍后才能知道。
當(dāng)然,你可以等到這些東西都確定后再發(fā)起 ajax(..) 請求,并且到那時(shí)再引用全局 URL 常量。但我們還有另一種選擇,就是創(chuàng)建一個(gè)已經(jīng)預(yù)設(shè) url 實(shí)參的函數(shù)引用。
我們將創(chuàng)建一個(gè)新函數(shù),其內(nèi)部仍然發(fā)起 ajax(..) 請求,此外在等待接收另外兩個(gè)實(shí)參的同時(shí),我們手動(dòng)將 ajax(..) 第一個(gè)實(shí)參設(shè)置成你關(guān)心的 API 地址。
function getPerson(data,cb) { ajax( "http://some.api/person", data, cb ); } function getOrder(data,cb) { ajax( "http://some.api/order", data, cb ); }
手動(dòng)指定這些外層函數(shù)當(dāng)然是完全有可能的,但這可能會(huì)變得冗長乏味,特別是不同的預(yù)設(shè)實(shí)參還會(huì)變化的時(shí)候,譬如:
function getCurrentUser(cb) { getPerson( { user: CURRENT_USER_ID }, cb ); }
函數(shù)式編程者習(xí)慣于在重復(fù)做同一種事情的地方找到模式,并試著將這些行為轉(zhuǎn)換為邏輯可重用的實(shí)用函數(shù)。實(shí)際上,該行為肯定已是大多數(shù)讀者的本能反應(yīng)了,所以這并非函數(shù)式編程獨(dú)有。但是,對函數(shù)式編程而言,這個(gè)行為的重要性是毋庸置疑的。
為了構(gòu)思這個(gè)用于實(shí)參預(yù)設(shè)的實(shí)用函數(shù),我們不僅要著眼于之前提到的手動(dòng)實(shí)現(xiàn)方式,還要在概念上審視一下到底發(fā)生了什么。
用一句話來說明發(fā)生的事情:getOrder(data,cb) 是 ajax(url,data,cb) 函數(shù)的偏函數(shù)(partially-applied functions)。該術(shù)語代表的概念是:在函數(shù)調(diào)用現(xiàn)場(function call-site),將實(shí)參應(yīng)用(apply) 于形參。如你所見,我們一開始僅應(yīng)用了部分實(shí)參 —— 具體是將實(shí)參應(yīng)用到 url 形參 —— 剩下的實(shí)參稍后再應(yīng)用。
關(guān)于該模式更正式的說法是:偏函數(shù)嚴(yán)格來講是一個(gè)減少函數(shù)參數(shù)個(gè)數(shù)(arity)的過程;這里的參數(shù)個(gè)數(shù)指的是希望傳入的形參的數(shù)量。我們通過 getOrder(..) 把原函數(shù) ajax(..) 的參數(shù)個(gè)數(shù)從 3 個(gè)減少到了 2 個(gè)。
讓我們定義一個(gè) partial(..) 實(shí)用函數(shù):
function partial(fn,...presetArgs) { return function partiallyApplied(...laterArgs){ return fn( ...presetArgs, ...laterArgs ); }; }
建議: 只是走馬觀花是不行的。請花些時(shí)間研究一下該實(shí)用函數(shù)中發(fā)生的事情。請確保你真的理解了。由于在接下來的文章里,我們將會(huì)一次又一次地提到該模式,所以你最好現(xiàn)在就適應(yīng)它。
partial(..) 函數(shù)接收 fn 參數(shù),來表示被我們偏應(yīng)用實(shí)參(partially apply)的函數(shù)。接著,fn 形參之后,presetArgs 數(shù)組收集了后面?zhèn)魅氲膶?shí)參,保存起來稍后使用。
我們創(chuàng)建并 return 了一個(gè)新的內(nèi)部函數(shù)(為了清晰明了,我們把它命名為partiallyApplied(..)),該函數(shù)中,laterArgs 數(shù)組收集了全部實(shí)參。
你注意到在內(nèi)部函數(shù)中的 fn 和 presetArgs 引用了嗎?他們是怎么如何工作的?在函數(shù) partial(..) 結(jié)束運(yùn)行后,內(nèi)部函數(shù)為何還能訪問 fn 和 presetArgs 引用?你答對了,就是因?yàn)?strong>閉包!內(nèi)部函數(shù) partiallyApplied(..) 封閉(closes over)了 fn 和 presetArgs 變量,所以無論該函數(shù)在哪里運(yùn)行,在 partial(..) 函數(shù)運(yùn)行后我們?nèi)匀豢梢栽L問這些變量。所以理解閉包是多么的重要!
當(dāng) partiallyApplied(..) 函數(shù)稍后在某處執(zhí)行時(shí),該函數(shù)使用被閉包作用(closed over)的 fn 引用來執(zhí)行原函數(shù),首先傳入(被閉包作用的)presetArgs 數(shù)組中所有的偏應(yīng)用(partial application)實(shí)參,然后再進(jìn)一步傳入 laterArgs 數(shù)組中的實(shí)參。
如果你對以上感到任何疑惑,請停下來再看一遍。相信我,隨著我們進(jìn)一步深入本文,你會(huì)欣然接受這個(gè)建議。
提一句,對于這類代碼,函數(shù)式編程者往往喜歡使用更簡短的 => 箭頭函數(shù)語法(請看第 2 章的 “語法” 小節(jié)),像這樣:
var partial = (fn, ...presetArgs) => (...laterArgs) => fn( ...presetArgs, ...laterArgs );
毫無疑問這更加簡潔,甚至代碼稀少。但我個(gè)人覺得,無論我們從數(shù)學(xué)符號(hào)的對稱性上獲得什么好處,都會(huì)因函數(shù)變成了匿名函數(shù)而在整體的可讀性上失去更多益處。此外,由于作用域邊界變得模糊,我們會(huì)更加難以辯認(rèn)閉包。
不管你喜歡哪種語法實(shí)現(xiàn)方式,現(xiàn)在我們用 partial(..) 實(shí)用函數(shù)來制造這些之前提及的偏函數(shù):
var getPerson = partial( ajax, "http://some.api/person" ); var getOrder = partial( ajax, "http://some.api/order" );
請暫停并思考一下 getPerson(..) 函數(shù)的外形和內(nèi)在。它相當(dāng)于下面這樣:
var getPerson = function partiallyApplied(...laterArgs) { return ajax( "http://some.api/person", ...laterArgs ); };
創(chuàng)建 getOrder(..) 函數(shù)可以依葫蘆畫瓢。但是 getCurrentUser(..) 函數(shù)又如何呢?
// 版本 1 var getCurrentUser = partial( ajax, "http://some.api/person", { user: CURRENT_USER_ID } ); // 版本 2 var getCurrentUser = partial( getPerson, { user: CURRENT_USER_ID } );
我們可以(版本 1)直接通過指定 url 和 data 兩個(gè)實(shí)參來定義 getCurrentUser(..) 函數(shù),也可以(版本 2)將 getCurrentUser(..) 函數(shù)定義成 getPerson(..) 的偏應(yīng)用,該偏應(yīng)用僅指定一個(gè)附加的 data 實(shí)參。
因?yàn)榘姹?2 重用了已經(jīng)定義好的函數(shù),所以它在表達(dá)上更清晰一些。因此我認(rèn)為它更加貼合函數(shù)式編程精神。
版本 1 和 2 分別相當(dāng)于下面的代碼,我們僅用這些代碼來確認(rèn)一下對兩個(gè)函數(shù)版本內(nèi)部運(yùn)行機(jī)制的理解。
// 版本 1 var getCurrentUser = function partiallyApplied(...laterArgs) { return ajax( "http://some.api/person", { user: CURRENT_USER_ID }, ...laterArgs ); }; // 版本 2 var getCurrentUser = function outerPartiallyApplied(...outerLaterArgs) { var getPerson = function innerPartiallyApplied(...innerLaterArgs){ return ajax( "http://some.api/person", ...innerLaterArgs ); }; return getPerson( { user: CURRENT_USER_ID }, ...outerLaterArgs ); }
再強(qiáng)調(diào)一下,為了確保你理解這些代碼段發(fā)生了什么,請暫停并重新閱讀一下它們。
注意: 第二個(gè)版本的函數(shù)包含了一個(gè)額外的函數(shù)包裝層。這看起來有些奇怪而且多余,但對于你真正要適應(yīng)的函數(shù)式編程來說,這僅僅是它的冰山一角。隨著本文的繼續(xù)深入,我們將會(huì)把許多函數(shù)互相包裝起來。記住,這就是函數(shù)式編程!
我們接著看另外一個(gè)偏應(yīng)用的實(shí)用示例。設(shè)想一個(gè) add(..) 函數(shù),它接收兩個(gè)實(shí)參,并取二者之和:
function add(x,y) { return x + y; }
現(xiàn)在,想象我們要拿到一個(gè)數(shù)字列表,并且給其中每個(gè)數(shù)字加一個(gè)確定的數(shù)值。我們將使用 JS 數(shù)組對象內(nèi)置的 map(..) 實(shí)用函數(shù)。
[1,2,3,4,5].map( function adder(val){ return add( 3, val ); } ); // [4,5,6,7,8]
注意: 如果你沒見過 map(..) ,別擔(dān)心,我們會(huì)在本書后面的部分詳細(xì)介紹它。目前你只需要知道它用來循環(huán)遍歷(loop over)一個(gè)數(shù)組,在遍歷過程中調(diào)用函數(shù)產(chǎn)出新值并存到新的數(shù)組中。
因?yàn)?add(..) 函數(shù)簽名不是 map(..) 函數(shù)所預(yù)期的,所以我們不直接把它傳入 map(..) 函數(shù)里。這樣一來,偏應(yīng)用就有了用武之地:我們可以調(diào)整 add(..) 函數(shù)簽名,以符合 map(..) 函數(shù)的預(yù)期。
[1,2,3,4,5].map( partial( add, 3 ) ); // [4,5,6,7,8]bind(..)
JavaScript 有一個(gè)內(nèi)建的 bind(..) 實(shí)用函數(shù),任何函數(shù)都可以使用它。該函數(shù)有兩個(gè)功能:預(yù)設(shè) this 關(guān)鍵字的上下文,以及偏應(yīng)用實(shí)參。
我認(rèn)為將這兩個(gè)功能混合進(jìn)一個(gè)實(shí)用函數(shù)是極其糟糕的決定。有時(shí)你不想關(guān)心 this 的綁定,而只是要偏應(yīng)用實(shí)參。我本人基本上從不會(huì)同時(shí)需要這兩個(gè)功能。
對于下面的方案,你通常要傳 null 給用來綁定 this 的實(shí)參(第一個(gè)實(shí)參),而它是一個(gè)可以忽略的占位符。因此,這個(gè)方案非常糟糕。
請看:
var getPerson = ajax.bind( null, "http://some.api/person" );
那個(gè) null 只會(huì)給我?guī)頍o盡的煩惱。
將實(shí)參順序顛倒回想我們之前調(diào)用 Ajax 函數(shù)的方式:ajax( url, data, cb )。如果要偏應(yīng)用 cb 而稍后再指定 data 和 url 參數(shù),我們應(yīng)該怎么做呢?我們可以創(chuàng)建一個(gè)可以顛倒實(shí)參順序的實(shí)用函數(shù),用來包裝原函數(shù)。
function reverseArgs(fn) { return function argsReversed(...args){ return fn( ...args.reverse() ); }; } // ES6 箭頭函數(shù)形式 var reverseArgs = fn => (...args) => fn( ...args.reverse() );
現(xiàn)在可以顛倒 ajax(..) 實(shí)參的順序了,接下來,我們不再從左邊開始,而是從右側(cè)開始偏應(yīng)用實(shí)參。為了恢復(fù)期望的實(shí)參順序,接著我們又將偏應(yīng)用實(shí)參后的函數(shù)顛倒一下實(shí)參順序:
var cache = {}; var cacheResult = reverseArgs( partial( reverseArgs( ajax ), function onResult(obj){ cache[obj.id] = obj; } ) ); // 處理后: cacheResult( "http://some.api/person", { user: CURRENT_USER_ID } );
好,我們來定義一個(gè)從右邊開始偏應(yīng)用實(shí)參(譯者注:以下簡稱右偏應(yīng)用實(shí)參)的 partialRight(..) 實(shí)用函數(shù)。我們將運(yùn)用和上面相同的技巧于該函數(shù)中:
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 } );
這個(gè) partialRight(..) 函數(shù)的實(shí)現(xiàn)方案不能保證讓一個(gè)特定的形參接收特定的被偏應(yīng)用的值;它只能確保將被這些值(一個(gè)或幾個(gè))當(dāng)作原函數(shù)最右邊的實(shí)參(一個(gè)或幾個(gè))傳入。
舉個(gè)例子:
function foo(x,y,z) { var rest = [].slice.call( arguments, 3 ); console.log( x, y, z, rest ); } var f = partialRight( foo, "z:last" ); f( 1, 2 ); // 1 2 "z:last" [] f( 1 ); // 1 "z:last" undefined [] f( 1, 2, 3 ); // 1 2 3 ["z:last"] f( 1, 2, 3, 4 ); // 1 2 3 [4,"z:last"]
只有在傳兩個(gè)實(shí)參(匹配到 x 和 y 形參)調(diào)用 f(..) 函數(shù)時(shí),"z:last" 這個(gè)值才能被賦給函數(shù)的形參 z。在其他的例子里,不管左邊有多少個(gè)實(shí)參,"z:last" 都被傳給最右的實(shí)參。
一次傳一個(gè)我們來看一個(gè)跟偏應(yīng)用類似的技術(shù),該技術(shù)將一個(gè)期望接收多個(gè)實(shí)參的函數(shù)拆解成連續(xù)的鏈?zhǔn)胶瘮?shù)(chained functions),每個(gè)鏈?zhǔn)胶瘮?shù)接收單一實(shí)參(實(shí)參個(gè)數(shù):1)并返回另一個(gè)接收下一個(gè)實(shí)參的函數(shù)。
這就是柯里化(currying)技術(shù)。
首先,想象我們已創(chuàng)建了一個(gè) ajax(..) 的柯里化版本。我們這樣使用它:
curriedAjax( "http://some.api/person" ) ( { user: CURRENT_USER_ID } ) ( function foundUser(user){ /* .. */ } );
我們將三次調(diào)用分別拆解開來,這也許有助于我們理解整個(gè)過程:
var personFetcher = curriedAjax( "http://some.api/person" ); var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } ); getCurrentUser( function foundUser(user){ /* .. */ } );
該 curriedAjax(..) 函數(shù)在每次調(diào)用中,一次只接收一個(gè)實(shí)參,而不是一次性接收所有實(shí)參(像 ajax(..) 那樣),也不是先傳部分實(shí)參再傳剩余部分實(shí)參(借助 partial(..) 函數(shù))。
柯里化和偏應(yīng)用相似,每個(gè)類似偏應(yīng)用的連續(xù)柯里化調(diào)用都把另一個(gè)實(shí)參應(yīng)用到原函數(shù),一直到所有實(shí)參傳遞完畢。
不同之處在于,curriedAjax(..) 函數(shù)會(huì)明確地返回一個(gè)期望只接收下一個(gè)實(shí)參 data 的函數(shù)(我們把它叫做 curriedGetPerson(..)),而不是那個(gè)能接收所有剩余實(shí)參的函數(shù)(像此前的 getPerson(..) 函數(shù)) 。
如果一個(gè)原函數(shù)期望接收 5 個(gè)實(shí)參,這個(gè)函數(shù)的柯里化形式只會(huì)接收第一個(gè)實(shí)參,并且返回一個(gè)用來接收第二個(gè)參數(shù)的函數(shù)。而這個(gè)被返回的函數(shù)又只接收第二個(gè)參數(shù),并且返回一個(gè)接收第三個(gè)參數(shù)的函數(shù)。依此類推。
由此而知,柯里化將一個(gè)多參數(shù)(higher-arity)函數(shù)拆解為一系列的單元鏈?zhǔn)胶瘮?shù)。
如何定義一個(gè)用來柯里化的實(shí)用函數(shù)呢?我們將要用到第 2 章中的一些技巧。
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 ); } }; })( [] ); }
ES6 箭頭函數(shù)版本:
var curry = (fn, arity = fn.length, nextCurried) => (nextCurried = prevArgs => nextArg => { var args = prevArgs.concat( [nextArg] ); if (args.length >= arity) { return fn( ...args ); } else { return nextCurried( args ); } } )( [] );
此處的實(shí)現(xiàn)方式是把空數(shù)組 [] 當(dāng)作 prevArgs 的初始實(shí)參集合,并且將每次接收到的 nextArg 同 prevArgs 連接成 args 數(shù)組。當(dāng) args.length 小于 arity(原函數(shù) fn(..) 被定義和期望的形參數(shù)量)時(shí),返回另一個(gè) curried(..) 函數(shù)(譯者注:這里指代 nextCurried(..) 返回的函數(shù))用來接收下一個(gè) nextArg 實(shí)參,與此同時(shí)將 args 實(shí)參集合作為唯一的 prevArgs 參數(shù)傳入 nextCurried(..) 函數(shù)。一旦我們收集了足夠長度的 args 數(shù)組,就用這些實(shí)參觸發(fā)原函數(shù) fn(..)。
默認(rèn)地,我們的實(shí)現(xiàn)方案基于下面的條件:在拿到原函數(shù)期望的全部實(shí)參之前,我們能夠通過檢查將要被柯里化的函數(shù)的 length 屬性來得知柯里化需要迭代多少次。
假如你將該版本的 curry(..) 函數(shù)用在一個(gè) length 屬性不明確的函數(shù)上 —— 函數(shù)的形參聲明包含默認(rèn)形參值、形參解構(gòu),或者它是可變參數(shù)函數(shù),用 ...args 當(dāng)形參;參考第 2 章 —— 你將要傳入 arity 參數(shù)(作為 curry(..) 的第二個(gè)形參)來確保 curry(..) 函數(shù)的正常運(yùn)行。
我們用 curry(..) 函數(shù)來實(shí)現(xiàn)此前的 ajax(..) 例子:
var curriedAjax = curry( ajax ); var personFetcher = curriedAjax( "http://some.api/person" ); var getCurrentUser = personFetcher( { user: CURRENT_USER_ID } ); getCurrentUser( function foundUser(user){ /* .. */ } );
如上,我們每次函數(shù)調(diào)用都會(huì)新增一個(gè)實(shí)參,最終給原函數(shù) ajax(..) 使用,直到收齊三個(gè)實(shí)參并執(zhí)行 ajax(..) 函數(shù)為止。
還記得前面講到為數(shù)值列表的每個(gè)值加 3 的那個(gè)例子嗎?回顧一下,由于柯里化是和偏應(yīng)用相似的,所以我們可以用幾乎相同的方式以柯里化來完成那個(gè)例子。
[1,2,3,4,5].map( curry( add )( 3 ) ); // [4,5,6,7,8]
partial(add,3) 和 curry(add)(3) 兩者有什么不同呢?為什么你會(huì)選 curry(..) 而不是偏函數(shù)呢?當(dāng)你先得知 add(..) 是將要被調(diào)整的函數(shù),但如果這個(gè)時(shí)候并不能確定 3 這個(gè)值,柯里化可能會(huì)起作用:
var adder = curry( add ); // later [1,2,3,4,5].map( adder( 3 ) ); // [4,5,6,7,8]
讓我們來看看另一個(gè)有關(guān)數(shù)字的例子,這次我們拿一個(gè)列表的數(shù)字做加法:
function sum(...args) { var sum = 0; for (let i = 0; i < args.length; i++) { sum += args[i]; } return sum; } sum( 1, 2, 3, 4, 5 ); // 15 // 好,我們看看用柯里化怎么做: // (5 用來指定需要鏈?zhǔn)秸{(diào)用的次數(shù)) var curriedSum = curry( sum, 5 ); curriedSum( 1 )( 2 )( 3 )( 4 )( 5 ); // 15
這里柯里化的好處是,每次函數(shù)調(diào)用傳入一個(gè)實(shí)參,并生成另一個(gè)特定性更強(qiáng)的函數(shù),之后我們可以在程序中獲取并使用那個(gè)新函數(shù)。而偏應(yīng)用則是預(yù)先指定所有將被偏應(yīng)用的實(shí)參,產(chǎn)出一個(gè)等待接收剩下所有實(shí)參的函數(shù)。
如果想用偏應(yīng)用來每次指定一個(gè)形參,你得在每個(gè)函數(shù)中逐次調(diào)用 partialApply(..) 函數(shù)。而被柯里化的函數(shù)可以自動(dòng)完成這個(gè)工作,這讓一次多帶帶傳遞一個(gè)參數(shù)變得更加符合人機(jī)工程學(xué)。
在 JavaScript 中,柯里化和偏應(yīng)用都使用閉包來保存實(shí)參,直到收齊所有實(shí)參后我們再執(zhí)行原函數(shù)。
柯里化和偏應(yīng)用有什么用?無論是柯里化風(fēng)格(sum(1)(2)(3))還是偏應(yīng)用風(fēng)格(partial(sum,1,2)(3)),它們的簽名比普通函數(shù)簽名奇怪得多。那么,在適應(yīng)函數(shù)式編程的時(shí)候,我們?yōu)槭裁匆@么做呢?答案有幾個(gè)方面。
首先是顯而易見的理由,使用柯里化和偏應(yīng)用可以將指定分離實(shí)參的時(shí)機(jī)和地方獨(dú)立開來(遍及代碼的每一處),而傳統(tǒng)函數(shù)調(diào)用則需要預(yù)先確定所有實(shí)參。如果你在代碼某一處只獲取了部分實(shí)參,然后在另一處確定另一部分實(shí)參,這個(gè)時(shí)候柯里化和偏應(yīng)用就能派上用場。
另一個(gè)最能體現(xiàn)柯里化應(yīng)用的的是,當(dāng)函數(shù)只有一個(gè)形參時(shí),我們能夠比較容易地組合它們。因此,如果一個(gè)函數(shù)最終需要三個(gè)實(shí)參,那么它被柯里化以后會(huì)變成需要三次調(diào)用,每次調(diào)用需要一個(gè)實(shí)參的函數(shù)。當(dāng)我們組合函數(shù)時(shí),這種單元函數(shù)的形式會(huì)讓我們處理起來更簡單。我們將在后面繼續(xù)探討這個(gè)話題。
如何柯里化多個(gè)實(shí)參?到目前為止,我相信我給出的是我們能在 JavaScript 中能得到的,最精髓的柯里化定義和實(shí)現(xiàn)方式。
具體來說,如果簡單看下柯里化在 Haskell 語言中的應(yīng)用,我們會(huì)發(fā)現(xiàn)一個(gè)函數(shù)總是在一次柯里化調(diào)用中接收多個(gè)實(shí)參 —— 而不是接收一個(gè)包含多個(gè)值的元組(tuple,類似我們的數(shù)組)實(shí)參。
在 Haskell 中的示例:
foo 1 2 3
該示例調(diào)用了 foo 函數(shù),并且根據(jù)傳入的三個(gè)值 1、2 和 3 得到了結(jié)果。但是在 Haskell 中,函數(shù)會(huì)自動(dòng)被柯里化,這意味著我們傳入函數(shù)的值都分別傳入了多帶帶的柯里化調(diào)用。在 JS 中看起來則會(huì)是這樣:foo(1)(2)(3)。這和我此前講過的 curry(..) 風(fēng)格如出一轍。
注意: 在 Haskell 中,foo (1,2,3) 不是把三個(gè)值當(dāng)作多帶帶的實(shí)參一次性傳入函數(shù),而是把它們包含在一個(gè)元組(類似 JS 數(shù)組)中作為多帶帶實(shí)參傳入函數(shù)。為了正常運(yùn)行,我們需要改變 foo 函數(shù)來處理作為實(shí)參的元組。據(jù)我所知,在 Haskell 中我們沒有辦法在一次函數(shù)調(diào)用中將全部三個(gè)實(shí)參獨(dú)立地傳入,而需要柯里化調(diào)用每個(gè)函數(shù)。誠然,多次調(diào)用對于 Haskell 開發(fā)者來說是透明的,但對 JS 開發(fā)者來說,這在語法上更加一目了然。
基于以上原因,我認(rèn)為此前展示的 curry(..) 函數(shù)是一個(gè)對 Haskell 柯里化的可靠改編,我把它叫做 “嚴(yán)格柯里化”。
然而,我們需要注意,大多數(shù)流行的 JavaScript 函數(shù)式編程庫都使用了一種并不嚴(yán)格的柯里化(loose currying)定義。
具體來說,往往 JS 柯里化實(shí)用函數(shù)會(huì)允許你在每次柯里化調(diào)用中指定多個(gè)實(shí)參。回顧一下之前提到的 sum(..) 示例,松散柯里化應(yīng)用會(huì)是下面這樣:
var curriedSum = looseCurry( sum, 5 ); curriedSum( 1 )( 2, 3 )( 4, 5 ); // 15
可以看到,語法上我們節(jié)省了()的使用,并且把五次函數(shù)調(diào)用減少成三次,間接提高了性能。除此之外,使用 looseCurry(..) 函數(shù)的結(jié)果也和之前更加狹義的 curry(..) 函數(shù)一樣。我猜便利性和性能因素是眾框架允許多實(shí)參柯里化的原因。這看起來更像是品味問題。
注意: 松散柯里化允許你傳入超過形參數(shù)量(arity,原函數(shù)確認(rèn)或指定的形參數(shù)量)的實(shí)參。如果你將函數(shù)的參數(shù)設(shè)計(jì)成可配的或變化的,那么松散柯里化將會(huì)有利于你。例如,如果你將要柯里化的函數(shù)接收 5 個(gè)實(shí)參,松散柯里化依然允許傳入超過 5 個(gè)的實(shí)參(curriedSum(1)(2,3,4)(5,6)),而嚴(yán)格柯里化就不支持 curriedSum(1)(2)(3)(4)(5)(6)。
我們可以將之前的柯里化實(shí)現(xiàn)方式調(diào)整一下,使其適應(yīng)這種常見的更松散的定義:
function looseCurry(fn,arity = fn.length) { return (function nextCurried(prevArgs){ return function curried(...nextArgs){ var args = prevArgs.concat( nextArgs ); if (args.length >= arity) { return fn( ...args ); } else { return nextCurried( args ); } }; })( [] ); }
現(xiàn)在每個(gè)柯里化調(diào)用可以接收一個(gè)或多個(gè)實(shí)參了(收集在 nextArgs 數(shù)組中)。至于這個(gè)實(shí)用函數(shù)的 ES6 箭頭函數(shù)版本,我們就留作一個(gè)小練習(xí),有興趣的讀者可以模仿之前 curry(..) 函數(shù)的來完成。
反柯里化你也會(huì)遇到這種情況:拿到一個(gè)柯里化后的函數(shù),卻想要它柯里化之前的版本 —— 這本質(zhì)上就是想將類似 f(1)(2)(3) 的函數(shù)變回類似 g(1,2,3) 的函數(shù)。
不出意料的話,處理這個(gè)需求的標(biāo)準(zhǔn)實(shí)用函數(shù)通常被叫作 uncurry(..)。下面是簡陋的實(shí)現(xiàn)方式:
function uncurry(fn) { return function uncurried(...args){ var ret = fn; for (let i = 0; i < args.length; i++) { ret = ret( args[i] ); } return ret; }; } // ES6 箭頭函數(shù)形式 var uncurry = fn => (...args) => { var ret = fn; for (let i = 0; i < args.length; i++) { ret = ret( args[i] ); } return ret; };
警告: 請不要以為 uncurry(curry(f)) 和 f 函數(shù)的行為完全一樣。雖然在某些庫中,反柯里化使函數(shù)變成和原函數(shù)(譯者注:這里的原函數(shù)指柯里化之前的函數(shù))類似的函數(shù),但是凡事皆有例外,我們這里就有一個(gè)例外。如果你傳入原函數(shù)期望數(shù)量的實(shí)參,那么在反柯里化后,函數(shù)的行為(大多數(shù)情況下)和原函數(shù)相同。然而,如果你少傳了實(shí)參,就會(huì)得到一個(gè)仍然在等待傳入更多實(shí)參的部分柯里化函數(shù)。我們在下面的代碼中說明這個(gè)怪異行為。
function sum(...args) { var sum = 0; for (let i = 0; i < args.length; i++) { sum += args[i]; } return sum; } var curriedSum = curry( sum, 5 ); var uncurriedSum = uncurry( curriedSum ); curriedSum( 1 )( 2 )( 3 )( 4 )( 5 ); // 15 uncurriedSum( 1, 2, 3, 4, 5 ); // 15 uncurriedSum( 1, 2, 3 )( 4 )( 5 ); // 15
uncurry() 函數(shù)最為常見的作用對象很可能并不是人為生成的柯里化函數(shù)(例如上文所示),而是某些操作所產(chǎn)生的已經(jīng)被柯里化了的結(jié)果函數(shù)。我們將在本章后面關(guān)于 “無形參風(fēng)格” 的討論中闡述這種應(yīng)用場景。
只要一個(gè)實(shí)參設(shè)想你向一個(gè)實(shí)用函數(shù)傳入一個(gè)函數(shù),而這個(gè)實(shí)用函數(shù)會(huì)把多個(gè)實(shí)參傳入函數(shù),但可能你只希望你的函數(shù)接收單一實(shí)參。如果你有個(gè)類似我們前面提到被松散柯里化的函數(shù),它能接收多個(gè)實(shí)參,但你卻想讓它接收單一實(shí)參。那么這就是我想說的情況。
我們可以設(shè)計(jì)一個(gè)簡單的實(shí)用函數(shù),它包裝一個(gè)函數(shù)調(diào)用,確保被包裝的函數(shù)只接收一個(gè)實(shí)參。既然實(shí)際上我們是強(qiáng)制把一個(gè)函數(shù)處理成單參數(shù)函數(shù)(unary),那我們索性就這樣命名實(shí)用函數(shù):
function unary(fn) { return function onlyOneArg(arg){ return fn( arg ); }; } // ES6 箭頭函數(shù)形式 var unary = fn => arg => fn( arg );
我們此前已經(jīng)和 map(..) 函數(shù)打過照面了。它調(diào)用傳入其中的 mapping 函數(shù)時(shí)會(huì)傳入三個(gè)實(shí)參:value、index 和 list。如果你希望你傳入 map(..) 的 mapping 函數(shù)只接收一個(gè)參數(shù),比如 value,你可以使用 unary(..) 函數(shù)來操作:
function unary(fn) { return function onlyOneArg(arg){ return fn( arg ); }; } var adder = looseCurry( sum, 2 ); // 出問題了: [1,2,3,4,5].map( adder( 3 ) ); // ["41,2,3,4,5", "61,2,3,4,5", "81,2,3,4,5", "101, ... // 用 `unary(..)` 修復(fù)后: [1,2,3,4,5].map( unary( adder( 3 ) ) ); // [4,5,6,7,8]
另一種常用的 unary(..) 函數(shù)調(diào)用示例:
["1","2","3"].map( parseFloat ); // [1,2,3] ["1","2","3"].map( parseInt ); // [1,NaN,NaN] ["1","2","3"].map( unary( parseInt ) ); // [1,2,3]
對于 parseInt(str,radix) 這個(gè)函數(shù)調(diào)用,如果 map(..) 函數(shù)調(diào)用它時(shí)在它的第二個(gè)實(shí)參位置傳入 index,那么毫無疑問 parseInt(..) 會(huì)將 index 理解為 radix 參數(shù),這是我們不希望發(fā)生的。而 unary(..) 函數(shù)創(chuàng)建了一個(gè)只接收第一個(gè)傳入實(shí)參,忽略其他實(shí)參的新函數(shù),這就意味著傳入 index 不再會(huì)被誤解為 radix 參數(shù)。
傳一個(gè)返回一個(gè)說到只傳一個(gè)實(shí)參的函數(shù),在函數(shù)式編程工具庫中有另一種通用的基礎(chǔ)函數(shù):該函數(shù)接收一個(gè)實(shí)參,然后什么都不做,原封不動(dòng)地返回實(shí)參值。
function identity(v) { return v; } // ES6 箭頭函數(shù)形式 var identity = v => v;
看起來這個(gè)實(shí)用函數(shù)簡單到了無處可用的地步。但即使是簡單的函數(shù)在函數(shù)式編程的世界里也能發(fā)揮作用。就像演藝圈有句諺語:沒有小角色,只有小演員。
舉個(gè)例子,想象一下你要用正則表達(dá)式拆分(split up)一個(gè)字符串,但輸出的數(shù)組中可能包含一些空值。我們可以使用 filter(..) 數(shù)組方法(下文會(huì)詳細(xì)說到這個(gè)方法)來篩除空值,而我們將 identity(..) 函數(shù)作為 filter(..) 的斷言:
var words = " Now is the time for all... ".split( /s|/ ); words; // ["","Now","is","the","time","for","all","...",""] words.filter( identity ); // ["Now","is","the","time","for","all","..."]
既然 identity(..) 會(huì)簡單地返回傳入的值,而 JS 會(huì)將每個(gè)值強(qiáng)制轉(zhuǎn)換為 true 或 false,這樣我們就能在最終的數(shù)組里對每個(gè)值進(jìn)行保存或排除。
小貼士: 像這個(gè)例子一樣,另外一個(gè)能被用作斷言的單實(shí)參函數(shù)是 JS 自有的 Boolean(..) 方法,該方法會(huì)強(qiáng)制把傳入值轉(zhuǎn)為 true 或 false。
另一個(gè)使用 identity(..) 的示例就是將其作為替代一個(gè)轉(zhuǎn)換函數(shù)(譯者注:transformation,這里指的是對傳入值進(jìn)行修改或調(diào)整,返回新值的函數(shù))的默認(rèn)函數(shù):
function output(msg,formatFn = identity) { msg = formatFn( msg ); console.log( msg ); } function upper(txt) { return txt.toUpperCase(); } output( "Hello World", upper ); // HELLO WORLD output( "Hello World" ); // Hello World
如果不給 output(..) 函數(shù)的 formatFn 參數(shù)設(shè)置默認(rèn)值,我們可以叫出老朋友 partialRight(..) 函數(shù):
var specialOutput = partialRight( output, upper ); var simpleOutput = partialRight( output, identity ); specialOutput( "Hello World" ); // HELLO WORLD simpleOutput( "Hello World" ); // Hello World
你也可能會(huì)看到 identity(..) 被當(dāng)作 map(..) 函數(shù)調(diào)用的默認(rèn)轉(zhuǎn)換函數(shù),或者作為某個(gè)函數(shù)數(shù)組的 reduce(..) 函數(shù)的初始值。我們將會(huì)在第 8 章中提到這兩個(gè)實(shí)用函數(shù)。
恒定參數(shù)Certain API 禁止直接給方法傳值,而要求我們傳入一個(gè)函數(shù),就算這個(gè)函數(shù)只是返回一個(gè)值。JS Promise 中的 then(..) 方法就是一個(gè) Certain API。很多人聲稱 ES6 箭頭函數(shù)可以當(dāng)作這個(gè)問題的 “解決方案”。但我這有一個(gè)函數(shù)式編程實(shí)用函數(shù)可以完美勝任該任務(wù):
function constant(v) { return function value(){ return v; }; } // or the ES6 => form var constant = v => () => v;
這個(gè)微小而簡潔的實(shí)用函數(shù)可以解決我們關(guān)于 then(..) 的煩惱:
p1.then( foo ).then( () => p2 ).then( bar ); // 對比: p1.then( foo ).then( constant( p2 ) ).then( bar );
警告: 盡管使用 () => p2 箭頭函數(shù)的版本比使用 constant(p2) 的版本更簡短,但我建議你忍住別用前者。該箭頭函數(shù)返回了一個(gè)來自外作用域的值,這和 函數(shù)式編程的理念有些矛盾。我們將會(huì)在后面第 5 章的 “減少副作用” 小節(jié)中提到這種行為帶來的陷阱。
擴(kuò)展在參數(shù)中的妙用在第 2 章中,我們簡要地講到了形參數(shù)組解構(gòu)?;仡櫼幌略撌纠?/p>
function foo( [x,y,...args] ) { // .. } foo( [1,2,3] );
在 foo(..) 函數(shù)的形參列表中,我們期望接收單一數(shù)組實(shí)參,我們要把這個(gè)數(shù)組拆解 —— 或者更貼切地說,擴(kuò)展(spread out)—— 成獨(dú)立的實(shí)參 x 和 y。除了頭兩個(gè)位置以外的參數(shù)值我們都會(huì)通過 ... 操作將它們收集在 args 數(shù)組中。
當(dāng)函數(shù)必須接收一個(gè)數(shù)組,而你卻想把數(shù)組內(nèi)容當(dāng)成多帶帶形參來處理的時(shí)候,這個(gè)技巧十分有用。
然而,有的時(shí)候,你無法改變原函數(shù)的定義,但想使用形參數(shù)組解構(gòu)。舉個(gè)例子,請思考下面的函數(shù):
function foo(x,y) { console.log( x + y ); } function bar(fn) { fn( [ 3, 9 ] ); } bar( foo ); // 失敗
你注意到為什么 bar(foo) 函數(shù)失敗了嗎?
我們將 [3,9] 數(shù)組作為單一值傳入 fn(..) 函數(shù),但 foo(..) 期望接收多帶帶的 x 和 y 形參。如果我們可以把 foo(..) 的函數(shù)聲明改變成 function foo([x,y]) { .. 那就好辦了?;蛘?,我們可以改變 bar(..) 函數(shù)的行為,把調(diào)用改成 fn(...[3,9]),這樣就能將 3 和 9 分別傳入 foo(..) 函數(shù)了。
假設(shè)有兩個(gè)在此方法上互不兼容的函數(shù),而且由于各種原因你無法改變它們的聲明和定義。那么你該如何一并使用它們呢?
為了調(diào)整一個(gè)函數(shù),讓它能把接收的單一數(shù)組擴(kuò)展成各自獨(dú)立的實(shí)參,我們可以定義一個(gè)輔助函數(shù):
function spreadArgs(fn) { return function spreadFn(argsArr) { return fn( ...argsArr ); }; } // ES6 箭頭函數(shù)的形式: var spreadArgs = fn => argsArr => fn( ...argsArr );
注意: 我把這個(gè)輔助函數(shù)叫做 spreadArgs(..),但一些庫,比如 Ramda,經(jīng)常把它叫做 apply(..)。
現(xiàn)在我們可以使用 spreadArgs(..) 來調(diào)整 foo(..) 函數(shù),使其作為一個(gè)合適的輸入?yún)?shù)并正常地工作:
bar( spreadArgs( foo ) ); // 12
相信我,雖然我不能講清楚這些問題出現(xiàn)的原因,但它們一定會(huì)出現(xiàn)的。本質(zhì)上,spreadArgs(..) 函數(shù)使我們能夠定義一個(gè)借助數(shù)組 return 多個(gè)值的函數(shù),不過,它讓這些值仍然能分別作為其他函數(shù)的輸入?yún)?shù)來處理。
一個(gè)函數(shù)的輸出作為另外一個(gè)函數(shù)的輸入被稱作組合(composition),我們將在第四章詳細(xì)討論這個(gè)話題。
盡管我們在談?wù)?spreadArgs(..) 實(shí)用函數(shù),但我們也可以定義一下實(shí)現(xiàn)相反功能的實(shí)用函數(shù):
function gatherArgs(fn) { return function gatheredFn(...argsArr) { return fn( argsArr ); }; } // ES6 箭頭函數(shù)形式 var gatherArgs = fn => (...argsArr) => fn( argsArr );
注意: 在 Ramda 中,該實(shí)用函數(shù)被稱作 unapply(..),是與 apply(..) 功能相反的函數(shù)。我認(rèn)為術(shù)語 “擴(kuò)展(spread)” 和 “聚集(gather)” 可以把這兩個(gè)函數(shù)發(fā)生的事情解釋得更好一些。
因?yàn)橛袝r(shí)我們可能要調(diào)整一個(gè)函數(shù),解構(gòu)其數(shù)組形參,使其成為另一個(gè)分別接收多帶帶實(shí)參的函數(shù),所以我們可以通過使用 gatherArgs(..) 實(shí)用函數(shù)來將多帶帶的實(shí)參聚集到一個(gè)數(shù)組中。我們將在第 8 章中細(xì)說 reduce(..) 函數(shù),這里我們簡要說一下:它重復(fù)調(diào)用傳入的 reducer 函數(shù),其中 reducer 函數(shù)有兩個(gè)形參,現(xiàn)在我們可以將這兩個(gè)形參聚集起來:
function combineFirstTwo([ v1, v2 ]) { return v1 + v2; } [1,2,3,4,5].reduce( gatherArgs( combineFirstTwo ) ); // 15參數(shù)順序的那些事兒
對于多形參函數(shù)的柯里化和偏應(yīng)用,我們不得不通過許多令人懊惱的技巧來修正這些形參的順序。有時(shí)我們把一個(gè)函數(shù)的形參順序定義成柯里化需求的形參順序,但這種順序沒有兼容性,我們不得不絞盡腦汁來重新調(diào)整它。
讓人沮喪的可不僅是我們需要使用實(shí)用函數(shù)來委曲求全,在此之外,這種做法還會(huì)導(dǎo)致我們的代碼被無關(guān)代碼混淆。這種東西就像碎紙片,這一片那一片的,而不是一整個(gè)突出問題,但這些問題的細(xì)碎絲毫不會(huì)減少它們帶來的苦惱。
難道就沒有能讓我們從修正參數(shù)順序這件事里解脫出來的方法嗎?。?/p>
在第 2 章里,我們講到了命名實(shí)參(named-argument)解構(gòu)模式。回顧一下:
function foo( {x,y} = {} ) { console.log( x, y ); } foo( { y: 3 } ); // undefined 3
我們將 foo(..) 函數(shù)的第一個(gè)形參 —— 它被期望是一個(gè)對象 —— 解構(gòu)成多帶帶的形參 x 和 y。接著在調(diào)用時(shí)傳入一個(gè)對象實(shí)參,并且提供函數(shù)期望的屬性,這樣就可以把 “命名實(shí)參” 映射到相應(yīng)形參上。
命名實(shí)參主要的好處就是不用再糾結(jié)實(shí)參傳入的順序,因此提高了可讀性。我們可以發(fā)掘一下看看是否能設(shè)計(jì)一個(gè)等效的實(shí)用函數(shù)來處理對象屬性,以此提高柯里化和偏應(yīng)用的可讀性:
function partialProps(fn,presetArgsObj) { return function partiallyApplied(laterArgsObj){ return fn( Object.assign( {}, presetArgsObj, laterArgsObj ) ); }; } 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 ); } }; })( {} ); }
我們甚至不需要設(shè)計(jì)一個(gè) partialPropsRight(..) 函數(shù)了,因?yàn)槲覀兏静恍枰紤]屬性的映射順序,通過命名來映射形參完全解決了我們有關(guān)于順序的煩惱!
我們這樣使用這些使用函數(shù):
function foo({ x, y, z } = {}) { console.log( `x:${x} y:${y} z:${z}` ); } var f1 = curryProps( foo, 3 ); var f2 = partialProps( foo, { y: 2 } ); f1( {y: 2} )( {x: 1} )( {z: 3} ); // x:1 y:2 z:3 f2( { z: 3, x: 1 } ); // x:1 y:2 z:3
我們不用再為參數(shù)順序而煩惱了!現(xiàn)在,我們可以指定我們想傳入的實(shí)參,而不用管它們的順序如何。再也不需要類似 reverseArgs(..) 的函數(shù)或其它妥協(xié)了。贊!
屬性擴(kuò)展不幸的是,只有在我們可以掌控 foo(..) 的函數(shù)簽名,并且可以定義該函數(shù)的行為,使其解構(gòu)第一個(gè)參數(shù)的時(shí)候,以上技術(shù)才能起作用。如果一個(gè)函數(shù),其形參是各自獨(dú)立的(沒有經(jīng)過形參解構(gòu)),而且不能改變它的函數(shù)簽名,那我們應(yīng)該如何運(yùn)用這個(gè)技術(shù)呢?
function bar(x,y,z) { console.log( `x:${x} y:${y} z:${z}` ); }
就像之前的 spreadArgs(..) 實(shí)用函數(shù)一樣,我們也可以定義一個(gè) spreadArgProps(..) 輔助函數(shù),它接收對象實(shí)參的 key: value 鍵值對,并將其 “擴(kuò)展” 成獨(dú)立實(shí)參。
不過,我們需要注意某些異常的地方。我們使用 spreadArgs(..) 函數(shù)處理數(shù)組實(shí)參時(shí),參數(shù)的順序是明確的。然而,對象屬性的順序是不太明確且不可靠的。取決于不同對象的創(chuàng)建方式和屬性設(shè)置方式,我們無法完全確認(rèn)對象會(huì)產(chǎn)生什么順序的屬性枚舉。
針對這個(gè)問題,我們定義的實(shí)用函數(shù)需要讓你能夠指定函數(shù)期望的實(shí)參順序(比如屬性枚舉的順序)。我們可以傳入一個(gè)類似 ["x","y","z"] 的數(shù)組,通知實(shí)用函數(shù)基于該數(shù)組的順序來獲取對象實(shí)參的屬性值。
這著實(shí)不錯(cuò),但還是有點(diǎn)瑕疵,就算是最簡單的函數(shù),我們也免不了為其增添一個(gè)由屬性名構(gòu)成的數(shù)組。難道我們就沒有一種可以探知函數(shù)形參順序的技巧嗎?哪怕給一個(gè)普通而簡單的例子?還真有!
JavaScript 的函數(shù)對象上有一個(gè) .toString() 方法,它返回函數(shù)代碼的字符串形式,其中包括函數(shù)聲明的簽名。先忽略其正則表達(dá)式分析技巧,我們可以通過解析函數(shù)字符串來獲取每個(gè)多帶帶的命名形參。雖然這段代碼看起來有些粗暴,但它足以滿足我們的需求:
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] ) ); }; }
注意: 該實(shí)用函數(shù)的參數(shù)解析邏輯并非無懈可擊,使用正則來解析代碼這個(gè)前提就已經(jīng)很不靠譜了!但處理一般情況是我們的唯一目標(biāo),從這點(diǎn)來看這個(gè)實(shí)用函數(shù)還是恰到好處的。我們需要的只是對簡單形參(包括帶默認(rèn)值的形參)函數(shù)的形參順序做一個(gè)恰當(dāng)?shù)哪J(rèn)檢測。例如,我們的實(shí)用函數(shù)不需要把復(fù)雜的解構(gòu)形參給解析出來,因?yàn)闊o論如何我們不太可能對擁有這種復(fù)雜形參的函數(shù)使用 spreadArgProps() 函數(shù)。因此該邏輯能搞定 80% 的需求,它允許我們在其它不能正確解析復(fù)雜函數(shù)簽名的情況下覆蓋 propOrder 數(shù)組形參。這是本書盡可能尋找的一種實(shí)用性平衡。
讓我們看看 spreadArgProps(..) 實(shí)用函數(shù)是怎么用的:
function bar(x,y,z) { console.log( `x:${x} y:${y} z:${z}` ); } var f3 = curryProps( spreadArgProps( bar ), 3 ); var f4 = partialProps( spreadArgProps( bar ), { y: 2 } ); f3( {y: 2} )( {x: 1} )( {z: 3} ); // x:1 y:2 z:3 f4( { z: 3, x: 1 } ); // x:1 y:2 z:3
提個(gè)醒:本文中呈現(xiàn)的對象形參(object parameters)和命名實(shí)參(named arguments)模式,通過減少由調(diào)整實(shí)參順序帶來的干擾,明顯地提高了代碼的可讀性,不過據(jù)我所知,沒有哪個(gè)主流的函數(shù)式編程庫使用該方案。所以你會(huì)看到該做法與大多數(shù) JavaScript 函數(shù)式編程很不一樣.
此外,使用在這種風(fēng)格下定義的函數(shù)要求你知道每個(gè)實(shí)參的名字。你必須記?。骸斑@個(gè)函數(shù)形參叫作 ‘fn’ ”,而不是只記得:“噢,把這個(gè)函數(shù)作為第一個(gè)實(shí)參傳進(jìn)去”。
請小心地權(quán)衡它們。
無形參風(fēng)格在函數(shù)式編程的世界中,有一種流行的代碼風(fēng)格,其目的是通過移除不必要的形參-實(shí)參映射來減少視覺上的干擾。這種風(fēng)格的正式名稱為 “隱性編程(tacit programming)”,一般則稱作 “無形參(point-free)” 風(fēng)格。術(shù)語 “point” 在這里指的是函數(shù)形參。
警告: 且慢,先說明我們這次的討論是一個(gè)有邊界的提議,我不建議你在函數(shù)式編程的代碼里不惜代價(jià)地濫用無形參風(fēng)格。該技術(shù)是用于在適當(dāng)情況下提升可讀性。但你完全可能像濫用軟件開發(fā)里大多數(shù)東西一樣濫用它。如果你由于必須遷移到無參數(shù)風(fēng)格而讓代碼難以理解,請打住。你不會(huì)因此獲得小紅花,因?yàn)槟阌每此坡斆鞯逎y懂的方式抹除形參這個(gè)點(diǎn)的同時(shí),還抹除了代碼的重點(diǎn)。
我們從一個(gè)簡單的例子開始:
function double(x) { return x * 2; } [1,2,3,4,5].map( function mapper(v){ return double( v ); } ); // [2,4,6,8,10]
可以看到 mapper(..) 函數(shù)和 double(..) 函數(shù)有相同(或相互兼容)的函數(shù)簽名。形參(也就是所謂的 “point“)v 可以直接映射到 double(..) 函數(shù)調(diào)用里相應(yīng)的實(shí)參上。這樣,mapper(..) 函數(shù)包裝層是非必需的。我們可以將其簡化為無形參風(fēng)格:
function double(x) { return x * 2; } [1,2,3,4,5].map( double ); // [2,4,6,8,10]
回顧之前的一個(gè)例子:
["1","2","3"].map( function mapper(v){ return parseInt( v ); } ); // [1,2,3]
該例中,mapper(..) 實(shí)際上起著重要作用,它排除了 map(..) 函數(shù)傳入的 index 實(shí)參,因?yàn)槿绻贿@么做的話,parseInt(..) 函數(shù)會(huì)錯(cuò)把 index 當(dāng)作 radix 來進(jìn)行整數(shù)解析。該例子中我們可以借助 unary(..) 函數(shù):
["1","2","3"].map( unary( parseInt ) ); // [1,2,3]
使用無形參風(fēng)格的關(guān)鍵,是找到你代碼中,有哪些地方的函數(shù)直接將其形參作為內(nèi)部函數(shù)調(diào)用的實(shí)參。以上提到的兩個(gè)例子中,mapper(..) 函數(shù)拿到形參 v 多帶帶傳入了另一個(gè)函數(shù)調(diào)用。我們可以借助 unary(..) 函數(shù)將提取形參的邏輯層替換成無參數(shù)形式表達(dá)式。
警告: 你可能跟我一樣,已經(jīng)嘗試著使用 map(partialRight(parseInt,10)) 來將 10 右偏應(yīng)用為 parseInt(..) 的 radix 實(shí)參。然而,就像我們之前看到的那樣,partialRight(..) 僅僅保證將 10 當(dāng)作最后一個(gè)實(shí)參傳入原函數(shù),而不是將其指定為第二個(gè)實(shí)參。因?yàn)?map(..) 函數(shù)本身會(huì)將 3 個(gè)實(shí)參(value、index 和 arr)傳入它的映射函數(shù),所以 10 就會(huì)被當(dāng)成第四個(gè)實(shí)參傳入 parseInt(..) 函數(shù),而這個(gè)函數(shù)只會(huì)對頭兩個(gè)實(shí)參作出反應(yīng)。
來看另一個(gè)例子:
// 將 `console.log` 當(dāng)成一個(gè)函數(shù)使用 // 便于避免潛在的綁定問題 function output(txt) { console.log( txt ); } function printIf( predicate, msg ) { if (predicate( msg )) { output( msg ); } } function isShortEnough(str) { return str.length <= 5; } var msg1 = "Hello"; var msg2 = msg1 + " World"; printIf( isShortEnough, msg1 ); // Hello printIf( isShortEnough, msg2 );
現(xiàn)在,我們要求當(dāng)信息足夠長時(shí),將它打印出來,換而言之,我們需要一個(gè) !isShortEnough(..) 斷言。你可能會(huì)首先想到:
function isLongEnough(str) { return !isShortEnough( str ); } printIf( isLongEnough, msg1 ); printIf( isLongEnough, msg2 ); // Hello World
這太簡單了...但現(xiàn)在我們的重點(diǎn)來了!你看到了 str 形參是如何傳遞的嗎?我們能否不通過重新實(shí)現(xiàn) str.length 的檢查邏輯,而重構(gòu)代碼并使其變成無形參風(fēng)格呢?
我們定義一個(gè) not(..) 取反輔助函數(shù)(在函數(shù)式編程庫中又被稱作 complement(..)):
function not(predicate) { return function negated(...args){ return !predicate( ...args ); }; } // ES6 箭頭函數(shù)形式 var not = predicate => (...args) => !predicate( ...args );
接著,我們使用 not(..) 函數(shù)來定義無形參的 isLongEnough(..) 函數(shù):
var isLongEnough = not( isShortEnough ); printIf( isLongEnough, msg2 ); // Hello World
目前為止已經(jīng)不錯(cuò)了,但還能更進(jìn)一步。我們實(shí)際上可以將 printIf(..) 函數(shù)本身重構(gòu)成無形參風(fēng)格。
我們可以用 when(..) 實(shí)用函數(shù)來表示 if 條件句:
function when(predicate,fn) { return function conditional(...args){ if (predicate( ...args )) { return fn( ...args ); } }; } // ES6 箭頭函數(shù)形式 var when = (predicate,fn) => (...args) => predicate( ...args ) ? fn( ...args ) : undefined;
我們把本章前面講到的另一些輔助函數(shù)和 when(..) 函數(shù)結(jié)合起來搞定無形參風(fēng)格的 printIf(..) 函數(shù):
var printIf = uncurry( rightPartial( when, output ) );
我們是這么做的:將 output 方法右偏應(yīng)用為 when(..) 函數(shù)的第二個(gè)(fn 形參)實(shí)參,這樣我們得到了一個(gè)仍然期望接收第一個(gè)實(shí)參(predicate 形參)的函數(shù)。當(dāng)該函數(shù)被調(diào)用時(shí),會(huì)產(chǎn)生另一個(gè)期望接收(譯者注:需要被打印的)信息字符串的函數(shù),看起來就是這樣:fn(predicate)(str)。
多個(gè)(兩個(gè))鏈?zhǔn)胶瘮?shù)的調(diào)用看起來很挫,就像被柯里化的函數(shù)。于是我們用 uncurry(..) 函數(shù)處理它,得到一個(gè)期望接收 str 和 predicate 兩個(gè)實(shí)參的函數(shù),這樣該函數(shù)的簽名就和 printIf(predicate,str) 原函數(shù)一樣了。
我們把整個(gè)例子復(fù)盤一下(假設(shè)我們本章已經(jīng)講解的實(shí)用函數(shù)都在這里了):
function output(msg) { console.log( msg ); } function isShortEnough(str) { return str.length <= 5; } var isLongEnough = not( isShortEnough ); var printIf = uncurry( partialRight( when, output ) ); var msg1 = "Hello"; var msg2 = msg1 + " World"; printIf( isShortEnough, msg1 ); // Hello printIf( isShortEnough, msg2 ); printIf( isLongEnough, msg1 ); printIf( isLongEnough, msg2 ); // Hello World
但愿無形參風(fēng)格編程的函數(shù)式編程實(shí)踐逐漸變得更有意義。你仍然可以通過大量實(shí)踐來訓(xùn)練自己,讓自己接受這種風(fēng)格。再次提醒,請三思而后行,掂量一下是否值得使用無形參風(fēng)格編程,以及使用到什么程度會(huì)益于提高代碼的可讀性。
有形參還是無形參,你怎么選?
注意: 還有什么無形參風(fēng)格編程的實(shí)踐呢?我們將在第 4 章的 “回顧形參” 小節(jié)里,站在新學(xué)習(xí)的組合函數(shù)知識(shí)之上來回顧這個(gè)技術(shù)。
總結(jié)偏應(yīng)用是用來減少函數(shù)的參數(shù)數(shù)量 —— 一個(gè)函數(shù)期望接收的實(shí)參數(shù)量 —— 的技術(shù),它減少參數(shù)數(shù)量的方式是創(chuàng)建一個(gè)預(yù)設(shè)了部分實(shí)參的新函數(shù)。
柯里化是偏應(yīng)用的一種特殊形式,其參數(shù)數(shù)量降低為 1,這種形式包含一串連續(xù)的鏈?zhǔn)胶瘮?shù)調(diào)用,每個(gè)調(diào)用接收一個(gè)實(shí)參。當(dāng)這些鏈?zhǔn)秸{(diào)用指定了所有實(shí)參時(shí),原函數(shù)就會(huì)拿到收集好的實(shí)參并執(zhí)行。你同樣可以將柯里化還原。
其它類似 unary(..)、identity(..) 以及 constant(..) 的重要函數(shù)操作,是函數(shù)式編程基礎(chǔ)工具庫的一部分。
無形參是一種書寫代碼的風(fēng)格,這種風(fēng)格移除了非必需的形參映射實(shí)參邏輯,其目的在于提高代碼的可讀性和可理解性。
【上一章】翻譯連載 |《JavaScript 輕量級(jí)函數(shù)式編程》- 第 2 章:函數(shù)基礎(chǔ)
【下一章】翻譯連載 |《你不知道的JS》姊妹篇 |《JavaScript 輕量級(jí)函數(shù)式編程》- 第4章:組合函數(shù)
iKcamp原創(chuàng)新書《移動(dòng)Web前端高效開發(fā)實(shí)戰(zhàn)》已在亞馬遜、京東、當(dāng)當(dāng)開售。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/85184.html
摘要:我稱之為輕量級(jí)函數(shù)式編程。序眾所周知,我是一個(gè)函數(shù)式編程迷。函數(shù)式編程有很多種定義。本書是你開啟函數(shù)式編程旅途的絕佳起點(diǎn)。事實(shí)上,已經(jīng)有很多從頭到尾正確的方式介紹函數(shù)式編程的書了。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 譯者團(tuán)隊(duì)(排名不分先后):阿希、blueken、brucecham、...
摘要:一旦我們滿足了基本條件值為,我們將不再調(diào)用遞歸函數(shù),只是有效地執(zhí)行了。遞歸深諳函數(shù)式編程之精髓,最被廣泛引證的原因是,在調(diào)用棧中,遞歸把大部分顯式狀態(tài)跟蹤換為了隱式狀態(tài)。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;...
摘要:所以我覺得函數(shù)式編程領(lǐng)域更像學(xué)者的領(lǐng)域。函數(shù)式編程的原則是完善的,經(jīng)過了深入的研究和審查,并且可以被驗(yàn)證。函數(shù)式編程是編寫可讀代碼的最有效工具之一可能還有其他。我知道很多函數(shù)式編程編程者會(huì)認(rèn)為形式主義本身有助于學(xué)習(xí)。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液...
摘要:這就是積極的函數(shù)式編程。上一章翻譯連載第章遞歸下輕量級(jí)函數(shù)式編程你不知道的姊妹篇原創(chuàng)新書移動(dòng)前端高效開發(fā)實(shí)戰(zhàn)已在亞馬遜京東當(dāng)當(dāng)開售。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML 最堅(jiān)實(shí)的梁柱;分享,是 CSS 里最閃耀的一瞥;總...
摘要:從某些方面來講,這章回顧的函數(shù)知識(shí)并不是針對函數(shù)式編程者,非函數(shù)式編程者同樣需要了解。什么是函數(shù)針對函數(shù)式編程,很自然而然的我會(huì)想到從函數(shù)開始。如果你計(jì)劃使用函數(shù)式編程,你應(yīng)該盡可能多地使用函數(shù),而不是程序。指的是一個(gè)函數(shù)聲明的形參數(shù)量。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson?。 禮ou-Dont-Know-JS》作者 關(guān)于譯者:...
閱讀 1879·2021-11-25 09:43
閱讀 2155·2021-11-19 09:40
閱讀 3434·2021-11-18 13:12
閱讀 1748·2021-09-29 09:35
閱讀 670·2021-08-24 10:00
閱讀 2516·2019-08-30 15:55
閱讀 1720·2019-08-30 12:56
閱讀 1826·2019-08-28 17:59