摘要: JS函數(shù)式編程入門曹铃。
- 原文:學會使用函數(shù)式編程的程序員(第3部分)
- 作者:前端小智
Fundebug經授權轉載啤握,版權歸原作者所有。
本系列的其它篇:
引用透明 (Referential Transparency)
引用透明是一個富有想象力的優(yōu)秀術語,它是用來描述純函數(shù)可以被它的表達式安全的替換帽馋,通過下例來幫助我們理解谦炬。
在代數(shù)中,有一個如下的公式:
y = x + 10
接著:
x = 3
然后帶入表達式:
y = 3 + 10
注意這個方程仍然是有效的,我們可以利用純函數(shù)做一些相同類型的替換黍析。
下面是一個 JavaScript 的方法,在傳入的字符串兩邊加上單引號:
function quote (str) {
retrun "'" + str + "'"
}
下面是調用它:
function findError (key) {
return "不能找到 " + quote(key)
}
當查詢 key 值失敗時,findError 返回一個報錯信息卖怜。
因為 quote 是純函數(shù),我們可以簡單地將 quote 函數(shù)體(這里僅僅只是個表達式)替換掉在findError中的方法調用:
function findError (key) {
return "不能找到 " + "'" + str + "'"
}
這個就是通常所說的“反向重構”(它對我而言有更多的意義),可以用來幫程序員或者程序(例如編譯器和測試程序)推理代碼的過程一個很好的方法。如阐枣,這在推導遞歸函數(shù)時尤其有用的马靠。
執(zhí)行順序 (Execution Order)
大多數(shù)程序都是單線程的,即一次只執(zhí)行一段代碼蔼两。即使你有一個多線程程序甩鳄,大多數(shù)線程都被阻塞等待I/O完成,例如文件额划,網(wǎng)絡等等妙啃。
這也是當我們編寫代碼的時候,我們很自然考慮按次序來編寫代碼:
1. 拿到面包
2. 把2片面包放入烤面包機
3. 選擇加熱時間
4. 按下開始按鈕
5. 等待面包片彈出
6. 取出烤面包
7. 拿黃油
8. 拿黃油刀
9. 制作黃油面包
在這個例子中,有兩個獨立的操作:拿黃油以及 加熱面包。它們在 步驟9 時開始變得相互依賴俊戳。
我們可以將 步驟7 和 步驟8 與 步驟1 到 步驟6 同時執(zhí)行揖赴,因為它們彼此獨立。當我們開始做的時候,事情開始復雜了:
線程一
--------------------------
1. 拿到面包
2. 把2片面包放入烤面包機
3. 選擇加熱時間
4. 按下開始按鈕
5. 等待面包片彈出
6. 取出烤面包
線程二
-------------------------
1. 拿黃油
2. 拿黃油刀
3. 等待線程1完成
4. 取出烤面包
果線程1失敗抑胎,線程2怎么辦? 怎么協(xié)調這兩個線程? 烤面包這一步驟在哪個線程運行:線程1燥滑,線程2或者兩者?
不考慮這些復雜性,讓我們的程序保持單線程會更容易阿逃。但是,只要能夠提升我們程序的效率铭拧,要付出努力來寫好多線程程序,這是值得的。
然而恃锉,多線程有兩個主要問題:
- 多線程程序難于編寫搀菩、讀取、解釋破托、測試和調試肪跋。
- 一些語言,例如JavaScript,并不支持多線程,就算有些語言支持多線程,對它的支持也很弱。
但是炼团,如果順序無關緊要澎嚣,所有事情都是并行執(zhí)行的呢?
盡管這聽起來有些瘋狂,但其實并不像聽起來那么混亂。讓我們來看一下 Elm 的代碼來形象的理解它:
buildMessage message value =
let
upperMessage =
String.toUpper message
quotedValue =
"'" ++ value ++ "'"
in
upperMessage ++ ": " ++ quotedValue
這里的 buildMessage 接受參數(shù) message 和 value,然后,生成大寫的 message和 帶有引號的 value 瘟芝。
注意到 upperMessage 和 quotedValue 是獨立的易桃。我們怎么知道的呢?
在上面的代碼示例中,upperMessage 和 quotedValue 兩者都是純的并且沒有一個需要依賴其它的輸出。
如果它們不純锌俱,我們就永遠不知道它們是獨立的晤郑。在這種情況下,我們必須依賴程序中調用它們的順序來確定它們的執(zhí)行順序贸宏。這就是所有命令式語言的工作方式造寝。
第二點必須滿足的就是一個函數(shù)的輸出值不能作為其它函數(shù)的輸入值。如果存在這種情況,那么我們不得不等待其中一個完成才能執(zhí)行下一個吭练。
在本例中诫龙,upperMessage 和 quotedValue 都是純的并且沒有一個需要依賴其它的輸出,因此鲫咽,這兩個函數(shù)可以以任何順序執(zhí)行签赃。
編譯器可以在不需要程序員幫助的情況下做出這個決定。這只有在純函數(shù)式語言中才有可能分尸,因為很難(如果不是不可能的話)確定副作用的后果锦聊。
在純函數(shù)語言中,執(zhí)行的順序可以由編譯器決定箩绍。
考慮到 CPU 無法一再的加快速度孔庭,這種做法非常有利的。別一方面材蛛,生產商也不斷增加CPU內核芯片的數(shù)量圆到,這意味著代碼可以在硬件層面上并行執(zhí)行。使用純函數(shù)語言,就有希望在不改變任何代碼的情況下充分地發(fā)揮 CPU 芯片的功能并取得良好成效仰税。
類型注釋 (Type Annotations)
在靜態(tài)類型語言中构资,類型是內聯(lián)定義的。以下是 Java 代碼:
public static String quote(String str) {
return "'" + str + "'";
}
注意類型是如何同函數(shù)定義內聯(lián)在一起的陨簇。當有泛型時,它變的更糟:
private final Map<Integer, String> getPerson(Map<String, String> people, Integer personId) {
// ...
}
這里使用粗體標出了使它們使用的類型吐绵,但它們仍然會讓函數(shù)可讀性降低,你必須仔細閱讀才能找到變量的名稱河绽。
對于動態(tài)類型語言己单,這不是問題。在 Javascript 中耙饰,可以編寫如下代碼:
var getPerson = function(people, personId) {
// ...
};
這樣沒有任何的的類型信息更易于閱讀纹笼,唯一的問題就是放棄了類型檢測的安全特性。這樣能夠很簡單的傳入這些參數(shù),例如,一個 Number 類型的 people 以及一個 Objec t類型的 personId苟跪。
動態(tài)類型要等到程序執(zhí)行后才能知道哪里問題廷痘,這可能是在發(fā)布的幾個月后蔓涧。在 Java 中不會出現(xiàn)這種情況,因為它不能被編譯笋额。
但是,假如我們能同時擁有這兩者的優(yōu)異點呢? JavaScript 的語法簡單性以及 Java 的安全性元暴。
事實證明我們可以。下面是 Elm 中的一個帶有類型注釋的函數(shù):
add : Int -> Int -> Int
add x y =
x + y
請注意類型信息是在單獨的代碼行上面的兄猩,而正是這樣的分割使得其有所不同茉盏。
現(xiàn)在你可能認為類型注釋有錯訓。 第一次見到它的時候枢冤。 大都認為第一個 -> 應該是一個逗號鸠姨。可以加上隱含的括號,代碼就清晰多了:
add : Int -> (Int -> Int)
上例 add 是一個函數(shù)淹真,它接受類型為 Int 的單個參數(shù)讶迁,并返回一個函數(shù),該函數(shù)接受單個參數(shù) Int類型 并返回一個 Int 類型的結果核蘸。
以下是一個帶括號類型注釋的代碼:
doSomething : String -> (Int -> (String -> String))
doSomething prefix value suffix =
prefix ++ (toString value) ++ suffix
這里 doSomething 是一個函數(shù)添瓷,它接受 String 類型的單個參數(shù),然后返回一個函數(shù)值纱,該函數(shù)接受 Int 類型的單個參數(shù)鳞贷,然后返回一個函數(shù),該函數(shù)接受 String 類型的單個參數(shù)虐唠,并返回一個字符串搀愧。
注意為什么每個方法都只接受一個參數(shù)呢? 這是因為每個方法在 Elm 里面都是柯里化疆偿。
因為括號總是指向右邊咱筛,它們是不必要的,簡寫如下:
doSomething : String -> Int -> String -> String
當我們將函數(shù)作為參數(shù)傳遞時杆故,括號是必要的迅箩。如果沒有它們,類型注釋將是不明確的处铛。例如:
takes2Params : Int -> Int -> String
takes2Params num1 num2 =
-- do something
非常不同于:
takes1Param : (Int -> Int) -> String
takes1Param f =
-- do something
takes2Param 函數(shù)需要兩個參數(shù)饲趋,一個 Int 和另一個 Int,而takes1Param 函數(shù)需要一個參數(shù)撤蟆,這個參數(shù)為函數(shù), 這個函數(shù)需要接受兩個 Int 類型參數(shù)奕塑。
下面是 map 的類型注釋:
map : (a -> b) -> List a -> List b
map f list =
// ...
這里需要括號,因為 f 的類型是(a -> b)家肯,也就是說龄砰,函數(shù)接受類型 a 的單個參數(shù)并返回類型 b 的某個函數(shù)。
這里類型 a 是任何類型。當類型為大寫形式時换棚,它是顯式類型式镐,例如 String。當一個類型是小寫時固蚤,它可以是任何類型碟案。這里 a 可以是字符串,也可以是 Int颇蜡。
如果你看到 (a -> a) 那就是說輸入類型和輸出類型必須是相同的。它們是什么并不重要辆亏,但必須匹配风秤。
但在 map 這一示例中,有這樣一段 (a -> b)。這意味著它既能返回一個不同的類型,也能返回一個相同的類型扮叨。
但是一旦 a 的類型確定了,a 在整段代碼中就必須為這個類型缤弦。例如,如果 a 是一個 Int,b 是一個 String,那么這段代碼就相當于:
(Int -> String) -> List Int -> List String
這里所有的 a 都換成了 Int,所有的 b 都換成了 String彻磁。
List Int 類型意味著一個值都為 Int 類型的列表, List String 意味著一個值都為 String 類型的列表碍沐。如果你已經在 Java 或者其他的語言中使用過泛型,那么這個概念你應該是熟悉的
函數(shù)式 JavaScript
JavaScript 擁有很多類函數(shù)式的特性但它沒有純性,但是我們可以設法得到一些不變量和純函數(shù)衷蜓,甚至可以借助一些庫累提。
但這并不是理想的解決方法。如果你不得不使用純特性磁浇,為何不直接考慮函數(shù)式語言斋陪?
這并不理想,但如果你必須使用它置吓,為什么不從函數(shù)式語言中獲得一些好處呢?
不可變性(Immutability)
首先要考慮的是不變性无虚。在ES2015或ES6中,有一個新的關鍵詞叫const衍锚,這意味著一旦一個變量被設置友题,它就不能被重置:
const a = 1;
a = 2; // 這將在Chrome、Firefox或 Node中拋出一個類型錯誤戴质,但在Safari中則不會
在這里度宦,a 被定義為一個常量,因此一旦設置就不能更改告匠。這就是為什么 a = 2 拋出異常斗埂。
const 的缺陷在于它不夠嚴格,我們來看個例子:
const a = {
x: 1,
y: 2
};
a.x = 2; // 沒有異常
a = {}; // 報錯
注意到 a.x = 2 沒有拋出異常凫海。const 關鍵字唯一不變的是變量 a, a 所指向的對象是可變的呛凶。
那么Javascript中如何獲得不變性呢?
不幸的是,我們只能通過一個名為 Immutable.js 的庫來實現(xiàn)行贪。這可能會給我們帶來更好的不變性漾稀,但遺憾的是模闲,這種不變性使我們的代碼看起來更像 Java 而不是 Javascript。
柯里化與組合 (curring and composition)
在本系列的前面崭捍,我們學習了如何編寫柯里化函數(shù)尸折,這里有一個更復雜的例子:
const f = a => b => c => d => a + b + c + d
我們得手寫上述柯里化的過程,如下:
console.log(f(1)(2)(3)(4)); // prints 10
括號如此之多殷蛇,但這已經足夠讓Lisp程序員哭了实夹。有許多庫可以簡化這個過程,我最喜歡的是 Ramda粒梦。
使用 Ramda 簡化如下:
const f = R.curry((a, b, c, d) => a + b + c + d);
console.log(f(1, 2, 3, 4)); // prints 10
console.log(f(1, 2)(3, 4)); // also prints 10
console.log(f(1)(2)(3, 4)); // also prints 10
函數(shù)的定義并沒有好多少亮航,但是我們已經消除了對那些括號的需要。注意匀们,調用 f 時缴淋,可以指定任意參數(shù)。
重寫一下之前的 mult5AfterAdd10 函數(shù):
const add = R.curry((x, y) => x + y);
const mult5 = value => value * 5;
const mult5AfterAdd10 = R.compose(mult5, add(10));
事實上 Ramda 提供了很多輔助函數(shù)來做些簡單常見的運算泄朴,比如R.add以及R.multiply重抖。以上代碼我們還可以簡化:
const mult5AfterAdd10 = R.compose(R.multiply(5), R.add(10));
Map, Filter and Reduce
Ramda 也有自己的 map、filter和 reduce 版本祖灰。雖然這些函數(shù)存在于數(shù)組中钟沛。這幾個函數(shù)是在 Array.prototype 對象中的,而在 Ramda 中它們是柯里化的
const isOdd = R.flip(R.modulo)(2);
const onlyOdd = R.filter(isOdd);
const isEven = R.complement(isOdd);
const onlyEven = R.filter(isEven);
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(onlyEven(numbers)); // prints [2, 4, 6, 8]
console.log(onlyOdd(numbers)); // prints [1, 3, 5, 7]
R.modulo 接受2個參數(shù)局扶,被除數(shù)和除數(shù)讹剔。
isOdd 函數(shù)表示一個數(shù)除 2 的余數(shù)。若余數(shù)為 0详民,則返回 false延欠,即不是奇數(shù);若余數(shù)為 1沈跨,則返回 true由捎,是奇數(shù)。用 R.filp 置換一下 R.modulo 函數(shù)兩個參數(shù)順序饿凛,使得 2 作為除數(shù)狞玛。
isEven 函數(shù)是 isOdd 函數(shù)的補集。
onlyOdd 函數(shù)是由 isOdd 函數(shù)進行斷言的過濾函數(shù)涧窒。當它傳入最后一個參數(shù)心肪,一個數(shù)組,它就會被執(zhí)行纠吴。
同理硬鞍,onlyEven 函數(shù)是由 isEven 函數(shù)進行斷言的過濾函數(shù)。
當我們給函數(shù) onlyEven 和 onlyOd 傳入 numbers,isEven 和 isOdd 獲得了最后的參數(shù)固该,然后執(zhí)行最終返回我們期望的數(shù)字锅减。
Javascript的缺點
所有的庫和語言增強都已經得到了Javascript 的發(fā)展,但它仍然面臨著這樣一個事實:它是一種強制性的語言伐坏,它試圖為所有人提供所有的東西怔匣。
大多數(shù)前端開發(fā)人員都不得不使用 Javascript,因為這旨瀏覽器也識別的語言桦沉。相反每瞒,它們使用不同的語言編寫,然后編譯纯露,或者更準確地說剿骨,是把其它語言轉換成 Javascript。
CoffeeScript 是這類語言中最早的一批苔埋。目前,TypeScript 已經被 Angular2 采用蜒犯,Babel可以將這類語言編譯成 JavaScript组橄,越來越多的開發(fā)者在項目中采用這種方式。
但是這些語言都是從 Javascript 開始的罚随,并且只稍微改進了一點玉工。為什么不直接從純函數(shù)語言轉換到Javascript呢?
未來期盼
我們不可能知道未來會怎樣,但我們可以做一些有根據(jù)的猜測淘菩。以下是作者的一些看法:
- 能轉換成 JavaScript 這類語言會有更加豐富及健壯遵班。
- 已有40多年歷史的函數(shù)式編程思想將被重新發(fā)現(xiàn),以解決我們當前的軟件復雜性問題潮改。
- 目前的硬件狭郑,比如廉價的內存,快速的處理器汇在,使得函數(shù)式技術普及成為可能翰萨。
- PU不會變快,但是內核的數(shù)量會持續(xù)增加糕殉。
- 可變狀態(tài)將被認為是復雜系統(tǒng)中最大的問題之一亩鬼。
希望這系列文章能幫助你更好容易更好幫助你理解函數(shù)式編程及優(yōu)勢,作者相信函數(shù)式編程是未來趨勢阿蝶,大家有時間可以多多了解雳锋,接著提升你們的技能,然后未來有更好的出路羡洁。
原文:
編輯中可能存在的bug沒法實時知道玷过,事后為了解決這些bug,花了大量的時間進行l(wèi)og 調試,這邊順便給大家推薦一個好用的BUG監(jiān)控工具Fundebug。
你的點贊是我持續(xù)分享好東西的動力冶匹,歡迎點贊习劫!
一個笨笨的碼農,我的世界只能終身學習嚼隘!
更多內容請關注公眾號《大遷世界》诽里!
關于Fundebug
Fundebug專注于JavaScript、微信小程序飞蛹、微信小游戲谤狡、支付寶小程序、React Native卧檐、Node.js和Java線上應用實時BUG監(jiān)控墓懂。 自從2016年雙十一正式上線,F(xiàn)undebug累計處理了9億+錯誤事件霉囚,付費客戶有Google捕仔、360、金山軟件盈罐、百姓網(wǎng)等眾多品牌企業(yè)榜跌。歡迎大家免費試用!