【翻譯】JavaScript 中的函數(shù)式編程:函數(shù)、組合與柯里化
原文鏈接:https://blog.bitsrc.io/functional-programming-in-javascript-functions-composition-and-currying-3c765a50152e
豆皮粉兒,又見面啦!今天字節(jié)跳動數(shù)據(jù)平臺的"陽羨"小哥哥給大家?guī)硪黄g文章"JavaScript 中的函數(shù)式編程:函數(shù)、組合與柯里化",干貨滿滿,不容錯過!!!
本文作者:陽羨
面向?qū)ο缶幊蘙1]和函數(shù)式編程[2]是兩種截然不同的編程范式,有各自的規(guī)則,也有各自的優(yōu)缺點。
但是,JavaScript,并非一直使用一種編程范式,而是兼具兩者的特征。給你提供了普通 OOP 語言一些方面的能力,比如類、對象、繼承等。但同時,它也給你提供了一些函數(shù)式概念,比如高階函數(shù),也提供了組合與柯里化的能力。
高階函數(shù)
先說說我在本文中涉及的三個概念中最重要的一個:高階函數(shù)。
擁有對高階函數(shù)的訪問權(quán)意味著函數(shù)不僅僅是一個你可以從代碼中定義和調(diào)用的構(gòu)造,事實上,你可以將它們作為可賦值的變量,即函數(shù)是一等公民。
如果你寫過一些 JavaScript 的話,這一點應(yīng)該不會讓人感到驚訝,畢竟,你應(yīng)該已經(jīng)能夠簡單地從網(wǎng)上按照例子將匿名函數(shù)賦值給變量了。將函數(shù)賦值給變量在日常使用中并不罕見。

如果 JavaScript 是你用來學習編程的第一門語言,那么你可能會對上述邏輯在許多其他語言中是無效的而感到驚訝。像賦值一個整數(shù)一樣賦值一個函數(shù)其實非常有用,事實上,本文所涉及的大部分主題都是由此衍生而來。
高階函數(shù)的優(yōu)勢:封裝行為
通過高階函數(shù),我們不僅可以像上面一樣給變量賦值函數(shù),而且,我們還可以在函數(shù)調(diào)用時將其作為參數(shù)傳遞。這又為創(chuàng)建動態(tài)邏輯打開了大門,你可以通過直接將復(fù)雜的行為作為參數(shù)傳遞來重用邏輯。
想象一下,在一個純粹的面向?qū)ο蟮沫h(huán)境中工作,你需要重用一個邏輯,你知道一個基本邏輯可以被擴展并作為復(fù)雜邏輯的一部分。在這種情況下,你可能會選擇使用繼承,通過將該邏輯封裝在一個抽象類中,然后將其擴展為一組派生類,這些類利用該通用邏輯并對其進行補充。這是完美而高效的 OOP 準則,但讓我們看看我們剛才做了什么。我們:
創(chuàng)建了一個抽象的結(jié)構(gòu)來封裝我們的可重用邏輯。
創(chuàng)建了一個二級的派生類
讓后者在前者的基礎(chǔ)上進行邏輯擴展。
但是,在函數(shù)式的環(huán)境下,為了復(fù)用邏輯,我們可以簡單地將需要復(fù)用的邏輯提取到一個函數(shù)中,然后將這個函數(shù)作為參數(shù)傳遞給任何其他可以從這種封裝行為中受益的函數(shù)。我們只是在創(chuàng)建函數(shù),而不是創(chuàng)建“模版”。
下面的例子試圖展示我上面解釋的內(nèi)容。第一段代碼展示了你如何在 OOP 環(huán)境中去重用一個格式化并輸出的邏輯。

然而,第二個例子表明,通過將邏輯提取出來,并使用函數(shù)封裝,你可以用很少的成本來創(chuàng)建你所需要的邏輯。你可以繼續(xù)添加更多的格式化( format)和輸出(output)函數(shù),然后只需用一行代碼將它們組合在一起就可以了。

我的意思是,這兩種方法都有優(yōu)點,而且都是非常高效的,沒有高低優(yōu)劣之分。函數(shù)式有多么令人難以置信的靈活性,以及我們?nèi)绾问褂没镜暮瘮?shù)式原理,這僅僅是因為我們有能力將行為(即函數(shù))作為參數(shù)傳遞,就好像它們是一個基本類型,如整數(shù)或字符串。
高階函數(shù)的優(yōu)勢:整潔代碼
整潔代碼的最好例子就是數(shù)組方法,如 forEach ,map ,reduce 等。在非函數(shù)式語言中,例如 C 語言,迭代一個數(shù)組的元素,并對它們進行轉(zhuǎn)換,需要使用 for 循環(huán)或其他循環(huán)結(jié)構(gòu)。它們要求你以一種非常命令式的方式編寫代碼(換句話說,你需要表達事情如何在循環(huán)內(nèi)發(fā)生),而函數(shù)式則允許更多的聲明式編程風格(你最終指定需要發(fā)生什么)。

你的代碼實際上是在說:
聲明一個新的變量 i 作為 myArray 的索引 它的值范圍從 0 到 myArray 的數(shù)組長度。
遍歷i的值,然后把 i 位置的 myArray 的值乘 2,并將其添加到 transformedArray 數(shù)組中。
它當然是可行的,而且比較容易理解,但是,邏輯的復(fù)雜度會迅速升級,并且閱讀邏輯所需的認知成本也會增加。然而,表達同樣的邏輯,函數(shù)式可能更具有可讀性:

本質(zhì)上,這段代碼是說
用 double 函數(shù)映射 (map) myArray 的元素,并將結(jié)果賦值給 transformedArray。
因為邏輯被隱藏在兩個函數(shù)(map和double)中,所以你不必擔心理解它們的工作原理。你也可以在第一個例子中把乘法邏輯隱藏在一個函數(shù)里面,但是仍然需要暴露迭代邏輯,晦澀的迭代邏輯是你作為一個閱讀代碼的人,必須在頭腦中解析以理解其工作原理的重要部分。
柯里化(currying)
函數(shù)柯里化是指將一個多參數(shù)的函數(shù)變成一個少參數(shù)的函數(shù),并將部分參數(shù)固定下來的編程思想。讓我用一個例子來解釋。

現(xiàn)在,如果你想做的是將 10 加到一系列值上,你可以調(diào)用 add10,而不是每次都用相同的第二個參數(shù)調(diào)用 adder。我知道這可能是一個簡單的例子,當你尋找柯里化時,可能到處都是這個例子,但考慮到你正在做的事情:你正在利用 adder 函數(shù)的邏輯,并創(chuàng)建該函數(shù)的專門版本,換句話說,你正在擴展該函數(shù),就像你使用一個類一樣。
你可以把柯里化看作是函數(shù)式編程的繼承,按照這個思路,再回到上面格式化輸出的例子,你可以這樣編寫你的代碼:

本質(zhì)上,你有一個叫做 log 的函數(shù),它需要三個參數(shù),而我們把它柯里化成專門的版本,只需要一個,因為另外兩個已經(jīng)被我們選好了。
需要注意的是,我把 log 函數(shù)當作一個抽象類,只是因為在我的例子中,你不會想直接使用它,然而這樣做沒有任何限制,因為這只是一個普通的函數(shù)。如果我們使用的是抽象類,你就不能直接實例化它。
組合(Composition)
最后,函數(shù)組合是高階函數(shù)的另一個非常有趣的衍生品。乍一看,人們很容易把組成混淆為柯里化的情況,或者也許反過來說,有柯里化的函數(shù)而不是直接的值(就像我們在上面的記錄儀例子中做的那樣)可以被認為是函數(shù)組合。
這些觀點其實都沒有錯,當你開始使用函數(shù)式時,這兩個概念之間有一條非常細微的界限。具體來說,組成的定義如下:
在計算機科學[3]中,函數(shù)組合是一種將簡單的函數(shù)[4]組合起來建立更復(fù)雜的函數(shù)的行為或機制。與數(shù)學[5]中通常的函數(shù)組合[6]一樣,每個函數(shù)的結(jié)果作為下一個函數(shù)的參數(shù)傳遞,最后一個函數(shù)的結(jié)果就是整個函數(shù)的結(jié)果。
這是維基百科上關(guān)于函數(shù)組成的定義,最后加粗的部分是我強調(diào)的,因為那是關(guān)鍵部分。在柯里化中,你沒有這個限制,你可以很容易地使用預(yù)先設(shè)定的函數(shù)參數(shù),如果它們是函數(shù),它們不必一個接一個地調(diào)用,讓第一個函數(shù)的結(jié)果成為第二個函數(shù)的輸入,以此類推。
與柯里化不同,這是一個強大的工具,因為在這里,只有部分功能,每個功能都在完成一個特定的任務(wù),等待著被組成更大更復(fù)雜的東西。想想看,就好像函數(shù)是樂高積木一樣,通過組合,只要你把正確的邏輯柯里化,并以正確的順序組合在一起(即只要你以正確的順序組合成正確的函數(shù)),你就能創(chuàng)造出任何你能想到的東西。
如果你以前使用過 Linux 發(fā)行版,你可能已經(jīng)注意到,Linux 中的 CLI 工具遵循一個非常確定的模式:它們只做一件事,并且能夠從標準輸入中讀取結(jié)果,并將其輸出到標準輸出。因此,允許用戶將多個命令組合成一句功能強大的命令,例如:
$ cat myfile.txt | wc -l
如上所示,我是在讀取一個文件,并計算它的行數(shù),然而,如果以不同的方式或與其他命令組合,輸出可能會有很大的不同。同樣的情況也發(fā)生在函數(shù)上,如果你在設(shè)計函數(shù)的時候,讓一個函數(shù)的輸出可以成為另一個函數(shù)的輸入,你也可以像這樣組合它們。

看看上面和最后的例子,我創(chuàng)建了四個不同的處理字符串的函數(shù),并將它們組合成三個不同的函數(shù)。你可以通過組合函數(shù)來創(chuàng)建新函數(shù)。這就是組合的魅力所在。
仔細看一下代碼,有幾處值得關(guān)注。
有些函數(shù)(replace和findMatches)實際上是接受參數(shù)并返回一個函數(shù)。這是為了使它們更通用,由于JS將返回的函數(shù)的上下文與函數(shù)本身一起保存(即閉包),我們能夠?qū)⑦@些參數(shù)作為被返回函數(shù)的 "全局 "變量,并作為組合的一部分使用。
請注意 compose 函數(shù),它是利用 ES6 的結(jié)構(gòu)操作符,簡單地在函數(shù)參數(shù)上進行迭代,并執(zhí)行它們,然后將其結(jié)果發(fā)送給下一個函數(shù)。reduceRight 的使用保證了我們在函數(shù)列表中從右到左進行執(zhí)行,這就是為什么我總是把小寫字母加到最后一個。如果你想讓順序反過來,你可以直接使用 reduce 來代替。
結(jié)論
如果使用得當,高階函數(shù)以及柯里化和組合都是非常強大的工具。我知道,如果你不習慣于使用函數(shù)式的思維模式,而是更愿意使用類和對象,這些技術(shù)可能看起來有悖于直覺,但它們本質(zhì)上并不晦澀難懂,只是需要換個角度去思考。
感受并享受 JavaScript 的函數(shù)式編程吧!
你之前使用過這些工具嗎?你更喜歡用函數(shù)式編程思想寫代碼嗎?還是你更喜歡 OOP 開發(fā)?歡迎在評論區(qū)發(fā)表你的看法。
感謝閱讀,我們下期再見!
References
[1]?面向?qū)ο缶幊??https://en.wikipedia.org/wiki/Object-oriented_programming
[2]?函數(shù)式編程:?https://en.wikipedia.org/wiki/Functional_programming
[3]?計算機科學:?https://en.wikipedia.org/wiki/Computer_science
[4]?函數(shù):?https://en.wikipedia.org/wiki/Subroutine
[5]?數(shù)學:?https://en.wikipedia.org/wiki/Mathematics
[6]?函數(shù)組合:?https://en.wikipedia.org/wiki/Function_composition
