[一點總結(jié)]函數(shù)式編程入門 -- Curry, Compose

準(zhǔn)備給TWU的同學(xué)做一個關(guān)于函數(shù)式的Session,昨天在準(zhǔn)備Slides,順手總結(jié)一下,有正在學(xué)習(xí)的小伙伴可以做一個參考。

What

Functional Programming(函數(shù)式編程)在概念上和Object Oriented Programming(面向?qū)ο缶幊?, Procedural Programming(過程化編程)類似, 是一種編程范式。
與OOP以對象為中心的理念不同,F(xiàn)P將所有計算機的操作視為函數(shù)運算,函數(shù)是操作的基本單位。函數(shù)擁有和基本類型一樣的地位,可以將一個變量賦值為函數(shù)(First class -- 一等公民),可以在函數(shù)的參數(shù)中傳遞函數(shù)(higher-order function -- 高階函數(shù))

Why

  1. 學(xué)習(xí)一點新的編程范式可以有效防止老年癡呆。
  2. 真的很有趣
  3. 相比于過程化、面向?qū)ο?,函?shù)式書寫的代碼更易讀,更簡短。
  4. 因為函數(shù)式編程是無副作用(side effects)的,不需要考慮死鎖問題,適合并發(fā)編程,因此在云計算領(lǐng)域得到了廣泛應(yīng)用(Scala)

How

好了,進入正題
以下示例代碼均為JavaScript

1. 副作用--Side Effects

先來看兩段代碼

//代碼片段1
let minium = 20;
const checkAge = (age)=> age >= minium;
//代碼片段2
let number = 2;
const multipleNumber = (n) => {
  number = number * n;
  return number;
}

這兩段代碼有問題嗎?
通常情況下,代碼片段1并不會發(fā)生什么問題, 我們傳入年齡,并且判斷是不是大于20歲。

但如果有人修改了minium呢?此時判斷的條件改變了,導(dǎo)致我們的結(jié)果也會改變。當(dāng)我們第二次運行checkAge(22)的時候,可能返回的并不是第一次運行的結(jié)果。

對于checkAge這個函數(shù)來說,它需要觀測的值不僅有入?yún)?code>age,還有一個全局變量minium,它的運行結(jié)果依賴系統(tǒng)狀態(tài),這對于程序員來說是十分痛苦的。

而代碼片段2就很容易發(fā)現(xiàn)問題了,這個函數(shù)修改了一個全局變量,換言之,它修改了系統(tǒng)狀態(tài),當(dāng)?shù)诙屋斎胂嗤瑓?shù)的時候你會得到一個不一樣的結(jié)果。

不,這太讓人難過了,這不是我們想要的,我們希望我們的函數(shù)足夠純凈,相同的輸入永遠得到相同的輸出。而且,不要做多余的事:
偷偷在console里打一個log
偷偷給某個api發(fā)送一個request
偷偷修改本地文件系統(tǒng)

2. 純函數(shù)--Pure Function

Side Effects好嗎?我們心里都知道不好,但有的時候你不得不接受它,就像生活。
或許你的系統(tǒng)需要維護某些狀態(tài)來運行不同的程序,和它們接觸的函數(shù)就不可避免的變得不純凈起來。但這并不意味著我們就放棄治療了,把怪獸關(guān)在壁櫥里總比把它放出來到處搞破壞來得好。正所謂關(guān)注分離。

是時候引入純函數(shù)了。
我們把上面的代碼修改成這樣如何:

//代碼片段3
const checkAge = (age) => { const minium = 20; return age >= 20;}
//代碼片段4
const multipleNumber = (number, n) => {return number * n};

對于代碼片段3來說,現(xiàn)在它終于不用始終看著minium了,現(xiàn)在minium變成了它的一部分,永遠不會改變。這樣我們相同的輸入,永遠都會有相同的輸出。

對于代碼片段4,我們可能會有疑問:“原來我只用輸入一個參數(shù),現(xiàn)在我要輸入兩個參數(shù)了,有點不太對勁???”
“Emmmmm, 至少我們可以通過始終調(diào)用multipleNumber(2,n)或者給number一個默認(rèn)值來讓它變得純函數(shù)起來const multipleNumber(n,number=2)

所以純函數(shù)是什么呢?
Stateless(不改變狀態(tài),也不被狀態(tài)影響)

3. 柯里化 -- Curry

讓我們稍稍改變一下代碼片段4,這次讓我們消除疑慮。

//代碼片段5
const multipleNumber = (number) => (n) => {return number * n}

“教練,有什么區(qū)別?。课疫€是得傳兩個參數(shù)啊,唯一的區(qū)別是我得寫兩對括號了啊(multipleNumber(2)(n)?。?!”

哎呀別急,回頭看看文章開頭,"函數(shù)擁有和基本類型一樣的地位,可以將一個變量賦值為函數(shù)(First class -- 一等公民),可以在函數(shù)的參數(shù)中傳遞函數(shù)(higher-order function -- 高階函數(shù))。"

再看看代碼片段5,看到兩個=>有沒有想起什么?

我們有一整頁的時間思考
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……

答案是:
我們現(xiàn)在可以寫這樣的代碼了:

const multipleTwo = multipleNumber(2); // (n) => {return 2*n}
const result = multipleTwo(3);//6

我們把這樣拆分函數(shù)參數(shù) --> 形成一個高階函數(shù) 的過程,稱為柯里化。
這就是一個最簡單的柯里化的例子。
好處?

生成的高階函數(shù)變成了一個工廠,用來生產(chǎn)諸如multipleTwo這樣的函數(shù)。
有效消除重復(fù)代碼,誰用誰知道。

再舉一個例子:

const matchRegex = (regex, str) => str.match(regex);

將其柯里化之后得到一個叫matchRegex的工廠

const matchRegex = (regex) => (str) => str.match(regex);

我們可以生產(chǎn)這樣的函數(shù):

const hasSpace = match(/\s+\g);

對,matchRegex是沒有上下文含義的,但生成的函數(shù)根據(jù)傳入的正則產(chǎn)生了不同的業(yè)務(wù)含義。

*只傳給函數(shù)一部分參數(shù)也叫局部調(diào)用,當(dāng)然 和柯里化是完全不同的兩個東西

4. 組合 -- Compose

現(xiàn)在相信我們已經(jīng)對函數(shù)式編程有了一個初步的認(rèn)知。

想象這樣一個業(yè)務(wù)場景:
我們是Email Writer,
有時候我們需要把郵件的某些Text變成UPPERCASE,
const uppercase = (str) => str.toUpperCase();
有時候我們需要給Text末尾加上一個感嘆號,!
const addBondToEnd = (str) => str+"!";
有時候我們需要既把Text變成uppercase, 也要在后面加一個感嘆號:TEXT!

怎么辦呢?

//So Easy
const addBondToEndAndUpperCase=(str)=>{
  return addBondToEnd(uppercase(str));
};

把兩個函數(shù)都調(diào)用一遍不就好了。我們還生成了一個新函數(shù),每次想做這兩個操作的時候調(diào)用這個新的函數(shù)就行了。

真棒!我們已經(jīng)知道組合是什么東西了!

但多想一步,如果我們有許許多多這樣的基礎(chǔ)函數(shù)大概10000個,我們想要生成不同的組合函數(shù),我們還要這樣手寫新函數(shù)嗎?

我太懶了,我不想這么做,重復(fù)寫這樣的code讓我覺得生不如死。

讓我們回到文章開頭,"函數(shù)擁有和基本類型一樣的地位,可以將一個變量賦值為函數(shù)(First class -- 一等公民),可以在函數(shù)的參數(shù)中傳遞函數(shù)(higher-order function -- 高階函數(shù))。"

這次直接揭曉答案吧:
對于我這樣的懶人,需要的是這樣一個工具

const compose = (f, g) => (x) => f(g(x));

這樣我就可以這樣調(diào)用了:

const uppercase = (str) => str.toUpperCase();
const addBondToEnd = (str) => str+"!";
//addBondToEndAndUpperCase
compose(addBondToEnd, uppercase)("text");//TEXT!
//Of course you can assign it to a variable
const addBondToEndAndUpperCase = compose(addBondToEnd, uppercase)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容