準(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將所有計算機(jī)的操作視為函數(shù)運(yùn)算拳喻,函數(shù)是操作的基本單位。函數(shù)擁有和基本類型一樣的地位迹辐,可以將一個變量賦值為函數(shù)(First class -- 一等公民)
堪嫂,可以在函數(shù)的參數(shù)中傳遞函數(shù)(higher-order function -- 高階函數(shù))
。
Why
- 學(xué)習(xí)一點(diǎn)新的編程范式可以有效防止老年癡呆创千。
- 真的很有趣
- 相比于過程化缰雇、面向?qū)ο螅瘮?shù)式書寫的代碼更易讀追驴,更簡短械哟。
- 因?yàn)楹瘮?shù)式編程是無
副作用(side effects)
的,不需要考慮死鎖問題殿雪,適合并發(fā)編程暇咆,因此在云計算領(lǐng)域得到了廣泛應(yīng)用(Scala)
How
好了,進(jìn)入正題
以下示例代碼均為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)我們第二次運(yùn)行checkAge(22)
的時候钧忽,可能返回的并不是第一次運(yùn)行的結(jié)果。
對于checkAge
這個函數(shù)來說逼肯,它需要觀測的值不僅有入?yún)?code>age,還有一個全局變量minium
,它的運(yùn)行結(jié)果依賴系統(tǒng)狀態(tài)惰瓜,這對于程序員來說是十分痛苦的。
而代碼片段2就很容易發(fā)現(xiàn)問題了汉矿,這個函數(shù)修改了一個全局變量,換言之备禀,它修改了系統(tǒng)狀態(tài)洲拇,當(dāng)?shù)诙屋斎胂嗤瑓?shù)的時候你會得到一個不一樣的結(jié)果。
不曲尸,這太讓人難過了赋续,這不是我們想要的,我們希望我們的函數(shù)足夠純凈另患,相同的輸入永遠(yuǎn)得到相同的輸出纽乱。而且,不要做多余的事:
偷偷在console里打一個log
偷偷給某個api發(fā)送一個request
偷偷修改本地文件系統(tǒng)
2. 純函數(shù)--Pure Function
Side Effects好嗎昆箕?我們心里都知道不好鸦列,但有的時候你不得不接受它租冠,就像生活。
或許你的系統(tǒng)需要維護(hù)某些狀態(tài)來運(yùn)行不同的程序薯嗤,和它們接觸的函數(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變成了它的一部分,永遠(yuǎn)不會改變带射。這樣我們相同的輸入同规,永遠(yuǎn)都會有相同的輸出。
對于代碼片段4庸诱,我們可能會有疑問:“原來我只用輸入一個參數(shù)捻浦,現(xiàn)在我要輸入兩個參數(shù)了,有點(diǎn)不太對勁扒潘朱灿?”
“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)
H煊啊!咏雌!”
哎呀別急凡怎,回頭看看文章開頭,"函數(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)