學會JavaScript函數(shù)式編程(第3部分)

摘要: JS函數(shù)式編程入門曹铃。

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ù) messagevalue,然后,生成大寫的 message和 帶有引號的 value 瘟芝。

注意到 upperMessagequotedValue 是獨立的易桃。我們怎么知道的呢?

在上面的代碼示例中,upperMessagequotedValue 兩者都是純的并且沒有一個需要依賴其它的輸出。

如果它們不純锌俱,我們就永遠不知道它們是獨立的晤郑。在這種情況下,我們必須依賴程序中調用它們的順序來確定它們的執(zhí)行順序贸宏。這就是所有命令式語言的工作方式造寝。

第二點必須滿足的就是一個函數(shù)的輸出值不能作為其它函數(shù)的輸入值。如果存在這種情況,那么我們不得不等待其中一個完成才能執(zhí)行下一個吭练。

在本例中诫龙,upperMessagequotedValue 都是純的并且沒有一個需要依賴其它的輸出,因此鲫咽,這兩個函數(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 也有自己的 mapfilterreduce 版本祖灰。雖然這些函數(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ù) onlyEvenonlyOd 傳入 numbersisEvenisOdd 獲得了最后的參數(shù)固该,然后執(zhí)行最終返回我們期望的數(shù)字锅减。

Javascript的缺點

所有的庫和語言增強都已經得到了Javascript 的發(fā)展,但它仍然面臨著這樣一個事實:它是一種強制性的語言伐坏,它試圖為所有人提供所有的東西怔匣。

大多數(shù)前端開發(fā)人員都不得不使用 Javascript,因為這旨瀏覽器也識別的語言桦沉。相反每瞒,它們使用不同的語言編寫,然后編譯纯露,或者更準確地說剿骨,是把其它語言轉換成 Javascript。

CoffeeScript 是這類語言中最早的一批苔埋。目前,TypeScript 已經被 Angular2 采用蜒犯,Babel可以將這類語言編譯成 JavaScript组橄,越來越多的開發(fā)者在項目中采用這種方式。

但是這些語言都是從 Javascript 開始的罚随,并且只稍微改進了一點玉工。為什么不直接從純函數(shù)語言轉換到Javascript呢?

未來期盼

我們不可能知道未來會怎樣,但我們可以做一些有根據(jù)的猜測淘菩。以下是作者的一些看法:

  1. 能轉換成 JavaScript 這類語言會有更加豐富及健壯遵班。
  2. 已有40多年歷史的函數(shù)式編程思想將被重新發(fā)現(xiàn),以解決我們當前的軟件復雜性問題潮改。
  3. 目前的硬件狭郑,比如廉價的內存,快速的處理器汇在,使得函數(shù)式技術普及成為可能翰萨。
  4. PU不會變快,但是內核的數(shù)量會持續(xù)增加糕殉。
  5. 可變狀態(tài)將被認為是復雜系統(tǒng)中最大的問題之一亩鬼。

希望這系列文章能幫助你更好容易更好幫助你理解函數(shù)式編程及優(yōu)勢,作者相信函數(shù)式編程是未來趨勢阿蝶,大家有時間可以多多了解雳锋,接著提升你們的技能,然后未來有更好的出路羡洁。

原文:

  1. https://medium.com/@cscalfani...
  2. https://medium.com/@cscalfani...

編輯中可能存在的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è)榜跌。歡迎大家免費試用!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末盅粪,一起剝皮案震驚了整個濱河市钓葫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌票顾,老刑警劉巖础浮,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異奠骄,居然都是意外死亡豆同,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門含鳞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诱告,“玉大人,你說我怎么就攤上這事民晒【樱” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵潜必,是天一觀的道長靴姿。 經常有香客問我,道長磁滚,這世上最難降的妖魔是什么佛吓? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任宵晚,我火速辦了婚禮,結果婚禮上维雇,老公的妹妹穿的比我還像新娘淤刃。我一直安慰自己,他們只是感情好吱型,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布逸贾。 她就那樣靜靜地躺著,像睡著了一般津滞。 火紅的嫁衣襯著肌膚如雪铝侵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天触徐,我揣著相機與錄音咪鲜,去河邊找鬼。 笑死撞鹉,一個胖子當著我的面吹牛疟丙,可吹牛的內容都是我干的。 我是一名探鬼主播鸟雏,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼享郊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了崔慧?” 一聲冷哼從身側響起拂蝎,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤穴墅,失蹤者是張志新(化名)和其女友劉穎惶室,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玄货,經...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡皇钞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了松捉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夹界。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖隘世,靈堂內的尸體忽然破棺而出可柿,到底是詐尸還是另有隱情,我是刑警寧澤丙者,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布复斥,位于F島的核電站,受9級特大地震影響械媒,放射性物質發(fā)生泄漏目锭。R本人自食惡果不足惜评汰,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望痢虹。 院中可真熱鬧被去,春花似錦、人聲如沸奖唯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臭埋。三九已至踪央,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓢阴,已是汗流浹背畅蹂。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荣恐,地道東北人液斜。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像叠穆,于是被迫代替她去往敵國和親少漆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354