摘要:真正留給我們要實現的僅僅是返回另外一部分用于局部應用的一元函數罷了。總結各用一句話做個小結吧局部應用是一種轉換技巧,通過預先傳入一個或多個參數來把多元函數轉變為更少一些元的函數甚或是一元函數。
元(arity)局部應用(Partial Application,也譯作“偏應用”或“部分應用”)和局部
套用( Currying, 也譯作“柯里化”),是函數式編程范式中很常用的技巧。
本文著重于闡述它們的特點和(更重要的是)差異。
在后續的代碼示例中,會頻繁出現 unary(一元),binary(二元),
ternary(三元)或 polyadic(多元,即多于一元)以及 variadic(可變
元)等數學用語。在本文所表述的范圍內,它們都是用來描述函數的參數數量的。
先來一個“無聊”的例子,實現一個 map 函數:
function map(list, unaryFn) { return [].map.call(list, unaryFn); } function square(n) { return n * n; } map([2, 3, 5], square); // => [4, 9, 25]
這個例子當然缺乏實用價值,我們僅僅是仿造了數組的原型方法 map 而已,不
過類似的應用場景還是可以想象得到的。那么這個例子和局部應用有什么關聯呢?
以下是一些客觀陳述的事實(但是很重要,確保你看明白了):
我們的 map 是一個二元函數;
square 是一個一元函數;
調用我們的 map 時,我們傳入了兩個參數([2, 3, 5] 和 square),
這兩個參數都應用在 map 函數里,并返回給我們最終的結果。
簡單明了吧?由于 map 要兩個參數,我們也給了兩個參數,于是我們可以說:
map 函數 完全應用 了我們傳入的參數。
而所謂局部應用就像它的字面意思一樣,函數調用的時候只提供部分參數供其應用
——比方說上例,調用 map 的時候只傳給它一個參數。
可是這要怎么實現呢?
首先,我們把 map 包裝一下:
function mapWith(list, unaryFn) { return map(list, unaryFn); }
然后,我們把二元的包裝函數變成兩個層疊的一元函數:
function mapWith(unaryFn) { return function (list) { return map(list, unaryFn); }; }
于是,這個包裝函數就變成了:先接收一個參數,然后返回給我們一個函數來接受
第二個參數,最終再返回結果。也就是這樣:
mapWith(square)([2, 3, 5]); // => [4, 9, 25]
到目前為止,局部應用似乎沒有體現出什么特別的價值,然而如果我們把應用場景
稍微擴展一下的話……
var squareAll = mapWith(square); squareAll([2, 3, 5]); // => [4, 9, 25] squareAll([1, 4, 7, 6]); // => [1, 16, 49, 36]
我們把對象 square(函數即對象)作為部分參數應用在 map 函數中,得到一
個一元函數,即 squareAll,于是我們可以想怎么用就怎么用。這就是局部應用
,恰當的使用這個技巧會非常有用。
我們可以在局部應用的例子的基礎上繼續探索局部套用,首先把前面的 mapWith
稍微修整修整:
function wrapper(unaryFn) { return function(list) { return binaryFn(list, unaryFn); }; }
function wrapper(secondArg) { return function(firstArg) { return binaryFn(firstArg, secondArg); }; }
如上,我刻意把修整分作兩步來寫。第一步,我們把 map 用一個更抽象的
binaryFn 取代,暗示我們不局限于做數組映射,可以是任何一種二元函數的處
理;同時,最外層的 mapWith 也就沒有必要了,使用更抽象的 wrapper 取代
。第二步,既然用作處理的函數都抽象化了,傳入的參數自然也沒有必要限定其類
型,于是就得到了最終的形態。
接下來的思考非常關鍵,請跟緊咯!
考慮一下未修整前的形態,最里層的 map 是哪里來的?——那是我們在最開始
的時候自己定義的。然而到了修整后的形態,binaryFn 是個抽象的概念,此時
此刻我們并沒有對應的函數可以直接調用它,那么我們要如何提供這一步?
再包裝一層,把 binaryFn 作為參數傳進來——
1 function rightmostCurry(binaryFn) { 2 return function (secondArg) { 3 return function (firstArg) { 4 return binaryFn(firstArg, secondArg); 5 }; 6 }; 7 }
你是否意識到這其實就是函數式編程的本質(的體現形式之一)?
那么,局部套用是如何體現出來的呢?我們把一開始寫的那個 map 函數套用進
來玩玩:
var rightmostCurriedMap = rightmostCurry(map); var squareAll = rightmostCurriedMap(square); squareAll([2, 3, 5]); // => [4, 9, 25] squareAll([1, 4, 7, 6]); // => [1, 16, 49, 36]
最后三句和之前講局部應用的例子是一樣的,局部套用的體現就在第一句上。乍一
看,這貌似就是又多了一層局部應用而已啊?不,它們是有差別的!
對比一下兩個例子:
// 局部應用 function mapWith(unaryFn) { return function (list) { return map(list, unaryFn); }; } // 局部套用 1 function rightmostCurry(binaryFn) { 2 return function (secondArg) { 3 return function (firstArg) { 4 return binaryFn(firstArg, secondArg); 5 }; 6 }; 7 }
在局部應用的例子里,最內層的處理函數是確定的;換言之,我們對最終的處理方
式是有預期的。我們只是把傳入參數分批完成,以獲得:一)較大的應用靈活性;
二)更單純的函數調用形態。
而在局部套用的例子里,第 2~6 行還是局部應用——這沒差別;但是可以看出
最內層的處理在定義的時候其實是未知的,而第 1 行的目的是為了傳入用于最
終處理的函數。因此我們需要先傳入進行最終處理的函數,然后再給它分批傳入參
數(局部應用),以獲得更大的應用靈活性。
回過頭來解讀一下這兩個名詞:
局部應用: 返回最終結果的處理方式是限定的,每一層的函數調用所傳入
的參數都將逐次參與最終處理過程中去;
局部套用: 返回最終結果的處理方式是未知的,需要我們在使用的時候將
其作為參數傳入。
在前面的例子中,為什么要把局部套用函數命名為 rightmostCurry?另外,是
否還有與之對應的 leftmostCurry 呢?
請回頭再看一眼上例的第 2~6 行,會發現層疊的兩個一元函數先傳入
secondArg,再傳入 firstArg,而最內層的處理函數則是反過來的。如此一來
,我們先接受最右邊的,再接受最左邊的,這就叫最右形式的局部套用;反之則是
最左形式的局部套用。
即使在本文的例子里都使用二元參數,但其實多元也是一樣的,無非就是增加局
部應用的層疊數量;而可變元的應用也不難,完全可以用某種數據結構來封裝多
個元參數(如數組)然后再進行解構處理——ES6 的改進會讓這一點變得更加簡
單。
但是這又有什么實際意義呢?仔細對比下面兩個代碼示例:
function rightmostCurry(binaryFn) { return function (secondArg) { return function (firstArg) { return binaryFn(firstArg, secondArg); }; }; } var rightmostCurriedMap = rightmostCurry(map); function square(n) { return n * n; } var squareAll = rightmostCurriedMap(square); squareAll([2, 3, 5]); // => [4, 9, 25] squareAll([1, 4, 7, 6]); // => [1, 16, 49, 36]
function leftmostCurry(binaryFn) { return function (firstArg) { return function (secondArg) { return binaryFn(firstArg, secondArg); }; }; } var leftmostCurriedMap = leftmostCurry(map); function square(n) { return n * n; } function double(n) { return n + n; } var oneToThreeEach = leftmostCurriedMap([1, 2, 3]); oneToThreeEach(square); // => [1, 4, 9] oneToThreeEach(double); // => [2, 4, 6]
這兩個例子很容易理解,我想就無須贅述了。值得注意的是,由于“從左向右”的
處理更合邏輯一些,所以現實中最左形式的局部套用比較常見,而且習慣上直接把
最左形式的局部套用就叫做 curry,所以如果沒有顯式的 rightmost 出現,
那么就可以按照慣例認為它是最左形式的。
最后,何時用最左形式何時用最右形式?嗯……這個其實沒有規定的,完全取決于
你的應用場景更適合用哪種形式來表達。從上面的對比中可以發現同樣的局部套用
(都套用 map),最左形式和最右形式會對應用形態的語義化表達產生不同的影
響:
對于最右形式的應用,如 squareAll([...]),它的潛臺詞是:不管傳入
的是什么,把它們挨個都平方咯。從語義角度來看,square 是主體,而
傳入的數組是客體;
對于最左形式的應用,如 oneToThreeEach(...),不必說,自然是之前傳入
的 [1, 2, 3] 是主體,而之后傳入的 square 或 double 才是客體;
所以說,根據應用的場景來選擇最合適的形式吧,不必拘泥于特定的某種形式。
回到現實至此,我們已經把局部應用和局部套用的微妙差別分析的透徹了,但這更多的是理
論性質的研究罷了,現實中這兩者的界限則非常模糊——所以很多人習慣混為一談
也就不很意外了。
就拿 rightmostCurry 那個例子來說吧:
function rightmostCurry(binaryFn) { return function (secondArg) { return function (firstArg) { return binaryFn(firstArg, secondArg); }; }; }
像這樣局部套用摻雜著局部應用的代碼在現實中只能算是“半成品”,為什么呢?
因為你很快會發現這樣的尷尬:
var squareAll = rightmostCurry(map)(square); var doubleAll = rightmostCurry(map)(double);
像這樣的“先局部套用然后緊接著局部應用”的模式是非常普遍的,我們為什么不
進一步抽象化它呢?
對于普遍化的模式,人們習慣于給它一個命名。對于上面的例子,可分解描述為:
最右形式的局部套用
針對 map
一元
局部應用
理一理語序可以組合成:針對 map 的最右形式(局部套用)的一元局部應用。
真尼瑪的啰嗦!
實際上我們真正想做的是:先給 map 函數局部應用一個參數,返回的結果可以
繼續應用 map 需要的另外一個參數(當然,你可以把 map 替換成其他的函
數,這就是局部套用的職責表現了)。真正留給我們要實現的僅僅是返回另外一部
分用于局部應用的一元函數罷了。
因此按照函數式編程的習慣,rightmostCurry 可以簡化成:
function rightmostUnaryPartialApplication(binaryFn, secondArg) { return rightmostCurry(binaryFn, secondArg); }
先別管冗長的命名,接著我們套用局部應用的技巧,進一步改寫成更簡明易懂的形
式:
function rightmostUnaryPartialApplication(binaryFn, secondArg) { return function (firstArg) { return binaryFn(firstArg, secondArg); }; }
這才是你在現實中隨處可見的“完全形態”!至于冗長的命名,小問題啦:
var applyLast = rightmostUnaryPartialApplication; var squareAll = applyLast(map, square); var doubleAll = applyLast(map, double);
如此一來,最左形式的相似實現就可以無腦出爐了:
function applyFirst(binaryFn, firstArg) { return function (secondArg) { return binaryFn(firstArg, secondArg); }; }
其實這樣的代碼很多開發者都已經寫過無數次了,可是如果你請教這是什么寫法,
回答你“局部應用”或“局部套用”的都會有。對于初學者來說就容易鬧不清楚到
底有什么區別,久而久之就干脆認為是一回事兒了。不過現在你應該明白過來了,
這個完全體其實是“局部應用”和“局部套用”的綜合應用。
各用一句話做個小結吧:
局部應用(Partial Application):是一種轉換技巧,通過預先傳入一個或多
個參數來把多元函數轉變為更少一些元的函數甚或是一元函數。
局部套用(Currying):是一種解構技巧,用于把多元函數分解為多個可鏈式調
用的層疊式的一元函數,這種解構可以允許你在其中局部應用一個或多個參數,但
是局部套用本身不提供任何參數——它提供的是調用鏈里的最終處理函數。
后記:撰寫本文的時間跨度較長,期間參考的資料和代碼無法一一計數。但是
Raganwald 的書和博客 以及 Michael Fogue
的 Functional JavaScript 給
予我的幫助和指導是我難以忘記的,在此向兩位以及所有幫助我的大牛們致謝!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/85380.html
摘要:函數式編程,一看這個詞,簡直就是學院派的典范。所以這期周刊,我們就重點引入的函數式編程,淺入淺出,一窺函數式編程的思想,可能讓你對編程語言的理解更加融會貫通一些。但從根本上來說,函數式編程就是關于如使用通用的可復用函數進行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數式編程(Functional Programming),一...
摘要:本文主要介紹了中的閉包與局部套用功能,由國內管理平臺編譯呈現。譬如,認為給帶來了閉包特性就是其中之一。但是首先,我們將考慮如何利用閉包進行實現。很顯然,閉包打破了這一準則。這就是局部調用,它總是比閉包更為穩妥。 【編者按】本文作者為專注于自然語言處理多年的 Pierre-Yves Saumont,Pierre-Yves 著有30多本主講 Java 軟件開發的書籍,自2008開始供職于 ...
摘要:以此類推,不定參數的方程也就被稱為可變參數函數。一般來說,函數式編程中的值都被認為是不可變值。實現了函數的對象,即可以與其他對象進行對比判斷是否屬于同一類型,被稱為。半群一個擁有,即將另一個對象轉化為相同類型的函數,函數的對象稱為。 原文地址譯者的Github 系列文章地址本文原作者尚未全部完成,有興趣的可以到原文或者譯文地址關注更新 Functional Programming Ja...
摘要:尾聲除了以上特性,函數式編程中還有,等比較難以理解的概念,本文暫時不牽扯那么深,留待有興趣的人自行調查。 本文簡單介紹了一下函數式編程的各種基本特性,希望能夠對于準備使用函數式編程的人起到一定入門作用。 showImg(/img/bVyUGu); 函數式編程,一個一直以來都酷,很酷,非常酷的名詞。雖然誕生很早也炒了很多年但是一直都沒有造成很大的水花,不過近幾年來隨著多核,分布式,大數據...
摘要:每個函數表達式包括函數對象括號和傳入的實參組成。和作用都是動態改變函數體內指向,只是接受參數形式不太一樣。在定義函數時,形參指定為一個對象調用函數時,將整個對象傳入函數,無需關心每個屬性的順序。 函數 JavaScript中,函數指只定義一次,但可以多次被多次執行或調用的一段JavaScript代碼。與數組類似,JavaScript中函數是特殊的對象,擁有自身屬性和方法 每個函數對象...
閱讀 1355·2021-11-15 11:45
閱讀 3123·2021-09-27 13:36
閱讀 2867·2019-08-30 15:54
閱讀 984·2019-08-29 12:38
閱讀 2905·2019-08-29 11:22
閱讀 2983·2019-08-26 13:52
閱讀 2026·2019-08-26 13:30
閱讀 584·2019-08-26 10:37