如何編寫高質量的 JS 函式(3) --函數語言程式設計[理論篇]

vivo網際網路技術發表於2019-10-07
本文首發於 vivo網際網路技術 微信公眾號 
連結:https://mp.weixin.qq.com/s/EWSqZuujHIRyx8Eb2SSidQ
作者:楊昆

 【編寫高質量函式系列】中,

《如何編寫高質量的 JS 函式(1) -- 敲山震虎篇》介紹了函式的執行機制,此篇將會從函式的命名、註釋和魯棒性方面,闡述如何通過 JavaScript 編寫高質量的函式。

 《如何編寫高質量的 JS 函式(2)-- 命名/註釋/魯棒篇》從函式的命名、註釋和魯棒性方面,闡述如何通過 JavaScript編寫高質量的函式。

【 前 言 】

這是編寫高質量函式系列文章的函數語言程式設計篇。我們來說一說,如何運用函數語言程式設計來提高你的函式質量。

函數語言程式設計篇分為兩篇,分別是理論篇和實戰篇。此篇文章屬於理論篇,在本文中,我將通過背景加提問的方式,對函數語言程式設計的本質、目的、來龍去脈等方面進行一次清晰的闡述。

寫作邏輯

通過對計算機和程式語言發展史的闡述,找到函數語言程式設計的時代背景。通過對與函數語言程式設計強相關的人物介紹,來探尋和感受函數語言程式設計的那些不為人知的本質。

下面列一個簡要目錄:

一、背景介紹

  1. 計算機和程式語言的發展史

二、函數語言程式設計的 10 問

  1. 為什麼會有函式式語言?函式式語言是如何產生的?它存在的意義是什麼?
  2. lambda 演算系統是啥?lambda 具體說的是啥內容?lambda 和函式有啥聯絡?為啥會有 lambda 演算系統?
  3. 函數語言程式設計為什麼要用函式去實現?
  4. 函式式語言中,或者在函數語言程式設計中,函式二字的含義是什麼?它具備什麼能力?
  5. 函數語言程式設計的特性關鍵詞有哪些?
  6. 命令式和函數語言程式設計是對立的嗎?
  7. 按照 FP 思想,不能使用迴圈,那我們該如何去解決?
  8. 丟擲異常會產生副作用,但如果不丟擲異常,又該用什麼替代呢?
  9. 函數語言程式設計不允許使用可變狀態的嗎?如何沒有副作用的表達我們的程式?
  10. 為什麼函數語言程式設計建議消滅掉語句?

三、JavaScript 函數語言程式設計的 5 問

  1. 為什麼函數語言程式設計要避免使用 this
  2. JavaScript 中函式是一等公民, 就可以得出 JavaScript 是函式式語言嗎?為什麼說 JS 是多型語言?
  3. 為什麼 JS 函式內部可以使用 for 迴圈嗎?
  4. JS 函式是一等公民是啥意識?這樣做的目的是啥?
  5. 用 JS 進行函數語言程式設計的缺點是什麼?

四、總結

  1. 函數語言程式設計的未來。
簡要目錄介紹完啦,大家請和我一起往下看。

PS:我好像是一個在海邊玩耍的孩子,不時為拾到比通常更光滑的石子,或更美麗的貝殼而歡欣鼓舞,而展現在我面前的是完全未探明的的真理之海。

【 正 文 】

計算機和程式語言的發展史

計算機和程式語言的發展史是由人類主導的,去了解在這個過程中起到關鍵作用的人物是非常重要的。

下面我們一起來認識幾位起關鍵作用的超巨。

一、戴維·希爾伯特

點選圖片介紹: 戴維·希爾伯特

希爾伯特被稱為數學界的無冕之王 ,他是天才中的天才。

在我看來,希爾伯特最厲害的一點就是:他鼓舞大家去將證明過程純機械化,因為這樣,機器就可以通過形式語言推理出大量定理。

也正是他的堅持推動,形式語言才逐漸走向歷史的舞臺中央。

二、艾倫·麥席森·圖靈

點選圖片介紹: 艾倫·麥席森·圖靈

艾倫·麥席森·圖靈被稱為電腦科學之父。

我認為,他最偉大的成就,就是發明了圖靈機:

上圖所示,就是圖靈機的模型圖。

這裡我們注意一點:從圖中,我們會發現,每個小方格可儲存一個數字或者字母。這個資訊非常重要,大家可以思考一下。

PS: 等我介紹 馮·諾依曼 的時候,就會明白它們之間的聯絡。

三、阿隆佐·邱奇

點選圖片介紹: 阿隆佐·邱奇

阿隆佐·邱奇,艾倫·麥席森·圖靈的博導。

他最偉大的成就,就是:發明了 λ(lambda) 演算。

如上圖,就是 λ(lambda) 演算的基本形式。

阿隆佐·邱奇發明的 λ演算和圖靈發明的圖靈機,一起改寫了當今世界,形式語言的歷史。

思考: 邱奇的 λ演算 和圖靈的圖靈機,這兩者有什麼區別和聯絡?

四、馮·諾依曼

點選圖片介紹: 馮·諾依曼

馮·諾依曼被稱為計算機之父。

他提出了馮·諾依曼體系結構:

從上圖,我們可以看出:馮·諾依曼體系結構由運算器、控制器、儲存器、輸入裝置、輸出裝置五個部分組分組成。採用二進位制邏輯,程式儲存、執行作為計算機制造的三個原則。

注意一個資訊:我們知道,計算機底層指令都是由 0 和 1 組成,通過對 0 和 1 的 CRUD ,來完成各種計算操作。我們再看圖靈機,會發現其每個小方格可儲存一個數字或者字母。

看到這,是不是發現馮·諾依曼體系結構和圖靈機有一些聯絡。

是的,現馮·諾依曼體系結構就是按照圖靈機的模型來實現的計算機結構。計算機的 0 和 1 ,就是圖靈機的小方格中的數字或者字母的特例。

五、為什麼要提這些人

因為如果想徹底解開函數語言程式設計的困惑,那就必須要去了解這時代背景和關鍵人物的一些事蹟。

六、說一說 邱奇-圖靈論題

邱奇是圖靈的博士生導師,他們之間有一個著名的論題,那就是 邱奇-圖靈論題 。

論題大致的內容是:圖靈和 lambda 這兩種模型,有沒有一個模型能表示的計算,另一個模型表示不了呢?

到目前為止,這個論題還沒有答案。也正因為如此,讓很多人對 lambda 模型充滿了信心。後面的歲月中,lambda 模型一直在被很多人研究、論證、實踐。

七、第一臺可程式設計計算機的誕生

它叫 ENAIC

1946 年,世界上第一臺電子計算機—— ENIAC 問世,它可以改變計算方式,即可以更改程式。

也就是說:它是一臺可程式設計計算機。

八、為什麼要可程式設計

perl 語言的設計者 Larry Wall 說過:優秀的程式設計師具有三大美德:懶惰、急躁、傲慢。

可程式設計完美詮釋了懶惰的美德。在 ENAIC 誕生後,出現了各種各樣的 程式設計語言。三大美德也提現的淋漓盡致。

九、計算機語言的分類

上圖可以獲得以下資訊:

  1. 程式設計語言只是計算機語言的一個分類。
  2. HTML 、XML 是資料設計語言。
  3. 在程式設計語言中,分為說明式和宣告式。
  4. 在說明式中,又包含函式式、邏輯式等。其實 MySQL,就是邏輯式語言,它通過提問的方式來完成操作。
  5. 馮諾依曼體系更符合程式導向的語言。
這個分類可以好好看看,會有一些感受的。

十、簡單的程式設計語言發展史

上圖非常簡單明瞭,直到 1995 年。

時間線大概是這樣的:xxx ---> xxx ---> .... ---> JavaScript ...

時間來到了 1996 年,JavaScript 誕生了!

十一、JavaScript 誕生了!

1、JavaScript 之父——布蘭登·艾奇

圖中這位老哥叫布蘭登·艾奇 。那一年,他34歲。

2、看一看阮一峰寫的一段話

從上圖中你會有如下幾點感受:

  1. 第一個感受:阿布對 Java 一點興趣也沒有。
  2. 第二個感受:由於討厭 Java ,阿布不想用 Java 的物件表示形式,於是就借鑑了 Self 語言,使用基於原型的繼承機制。埋下了前幾年前端界用原型進行面對物件程式設計的種子。
  3. 第三個感受:阿布借鑑了 Scheme 語言,將函式提升到一等公民的地位,讓 JS 擁有了函數語言程式設計的能力。埋下了 JS 可以進行函數語言程式設計的種子。
  4. 第四個感受:JS 是既可以函數語言程式設計,也可以面對物件程式設計。

3、我個人的感受

我在回顧程式設計語言的發展史和一些故事後,我並不認為 JavaScript 是一個爛語言,相反正是這種中庸之道,才使得 JavaScript 能夠流行到現在。

十二、總結

通過對計算機語言的發展史和關鍵人物的簡潔介紹,我們可以從高層面去體會到函數語言程式設計在計算機語言發展史中的潛力和影響力。

不過,通過背景和人物的介紹,對函數語言程式設計的理解還是有限的。下面我將通過提問的方式來闡述函數語言程式設計的來龍去脈。

函數語言程式設計的 10 問

下面將通過 10 個問題的解答,來闡述函數語言程式設計的理論支撐、函數語言程式設計的誕生背景、函數語言程式設計的核心理論以及推導等知識。

一、為什麼會有函式式語言?函式式語言是如何產生的?它存在的意義是什麼?

函式式語言的存在,是為了實現運算系統的本質——運算。

1、形式化運算系統的研究

計算機未問世之前,四位大佬 阿蘭·圖靈、約翰 ·馮·諾依曼 、庫爾特 ·哥德爾 和阿隆左 ·丘奇。展開了對形式化的運算系統的研究。

通過形式系統來證明一個命題:可以用簡單的數學法則表達現實系統。

2、圖靈機和馮·諾依曼結構體系的缺陷

從上文的圖片和分析可知,圖靈機和馮諾依曼體系的計算機系統都依賴儲存(記憶體)進行運算。

換句話說就是:通過修改記憶體來反映運算的結果。並不是真正意義上的運算。

修改記憶體並不是我們想要的,我們想要的僅僅是運算。從目的性的角度看,修改記憶體可以說是運算系統中的副作用。或者說,是表現運算結果的一種手段。

這一切,圖靈的博導邱奇看在眼裡,他看到了問題的本質。為了實現運算系統的本質——運算,即不修改記憶體,直接通過運算拿到結果。

他提出了 lambda 演算的形式系統,一種更接近於運算才是本質的理論。

3、函式式語言和命令式語言的隔閡

從語言學分類來說:是兩種不同型別的計算範型。

從硬體系統來說:它們依賴於各自不同的計算機系統(也就是硬體)。為什麼依賴不同的硬體,是因為如果用馮諾依曼結構的計算機,就意味著要靠修改記憶體來實現運算。但是,這和 lambda 演算系統是相矛盾的。

因為基於 lambda 演算系統實現的函式式語言,是不需要暫存器的,也不存在需要使用暫存器去儲存變數的狀態。它只注重運算,運算結束,結果就會出來。

最大的隔閡就是依賴各自不同的計算機系統 。

4、計算機硬體的限制

目前為止,在技術上做不到基於 A 範型的計算機系統,同時支援 B 範型。也就是說,不能指望在 X86 指令集中出現適用於 lambda 演算 的指令、邏輯或者物理設計。

你可能會疑問,既然硬體不支援,那我們為什麼還能進行函數語言程式設計?

其實現實中,大多數人都是用的馮諾依曼體系的命令式語言。所以為了獲得特別的計算能力和程式設計特性。語言就在邏輯層虛擬一個環境,也因為這樣,誕生了 JS 這樣的多範型語言,以及 PY 這種指令碼語言。

究其根源,是因為,馮·諾依曼體系的計算機系統是基於儲存與指令系統的,並不是基於運算的。

5、黑暗中的曙光

在當時硬體裝置條件的限制下,邱奇提出的 lambda 演算,在很長時間內,都沒有被程式設計語言所實現。

直到馮諾依曼等人完成了 EDVAC 的十年之後。一位 MIT 的教授 John McCarthy 對邱奇的工作產生了興趣。在 1958 年,他公開了表處理語言 LISP 。這個 LISP 語言就是對邱奇的 lambda 演算的實現。

自此,世界上第一個函式式語言誕生了。

LISP 就是函式式語言的鼻祖,完成了 lamda 演算的實現,實現了 運算才是本質的運算系統

上圖是 Lisp 的圖片,感受一下圖片符號的魅力。

為什麼我說是曙光?

是因為,並沒有真正的勝利。此時的 LISP 依舊是工作在馮·諾依曼計算機上,因為當時只有這樣的計算機系統。

所以從 LISP 開始,函式式語言就是執行在解釋環境而非編譯環境中的。也就是傳說中的指令碼語言,直譯器語言。

6、真正的勝利

直到 1973 年,MIT 人工智慧實驗室的一組程式設計師開發了,被稱為 LISP 機器的硬體。自此,阿隆左·丘奇的 lambda 演算終於得到了 硬體實現。終於有一個計算機(硬體)系統可以宣稱在機器指令級別上支援了函式式語言。

7、總結

關於這問,我闡述了很多,從函式式語言誕生的目的、到函式式語言誕生的艱難過程、再到計算機硬體的限制。最後在不斷的努力下,做到了既可以通過直譯器,完成基於馮·諾依曼體系下,計算機系統的函數語言程式設計。也可以在機器指令級別上支援了函式式語言的計算機上進行純正的函數語言程式設計。

思考題:想一想,在如今,函數語言程式設計為什麼越來越被人所瞭解和掌握。

二、lambda 演算系統是啥?lambda 具體說的是啥內容?lambda 和函式有啥聯絡?為啥會有 lambda 演算系統

1、lamda 誕生的目的

lambda 是一種解決數學中的函式語義不清晰,很難表達清楚函式的結構層次的問題的運算方案。

也就是在運算過程中,不使用函式中的函式運算形式,而使用 lambda 的運算形式來進行運算。

2、lamda 簡單介紹

(1)一套用於研究函式定義、函式應用和遞迴的系統。

(2)函式式語言就是基於 lambda 運算而產生的運算範型。

3、函數語言程式設計的理論基石

lambda 演算系統是學習函數語言程式設計的一個非常重要的知識點。它是整個函數語言程式設計的理論基石。

4、數學中的函式

如下圖所示:

從上面的數學函式中,我們可以發現以下幾點:

  1. 沒有顯示給出函式的自變數
  2. 對定義和呼叫區分不嚴格。x2-2*x+1 既可以看成是函式 f(x) 的定義,又可以看成是函式 g(x) 對變數 x-1 的呼叫。

體會上面幾點,我們會發現:數學中的函式語義並不清晰,它很難表達清楚函式的結構層次。對此,邱奇給出瞭解決方法,他提出了 lambda(λ) 演算。

5、lambda(λ) 演算

基本定義形式:λ<變數>.<表示式>

通過這種方法定義的函式就叫 λ(lambda) 表示式。

我們可以把 lambda 翻譯成函式,即可以把 lambda 表示式念成函式表示式。

PS: 這裡說一下,函式式語言中的函式,是指 lambda(函式),它和我們現在的通用語言中,比如 C 中 的 function 是不同的兩個東西。

6、單個變數的栗子

λx.x2-2*x+1 就是一個 λ 表示式,其中顯式地指出了 x 是變數。將這個 λ 表示式定義應用於具體的變數值時,需要用一對括號把表示式括起來,當 x 是 1 時,如下所示:

(λx.x2-2*x+1)1

應用(也就是呼叫)過程,就是把變數值賦值給表示式中的 x ,並去掉 λ <變數>,過程如下:

(λx.x2-2*x+1)1=1-2*1+1=0

7、多個變數的栗子

表示式 λx.λy.x+y 中,有兩個變數 分別為 x 和 y。

當 x=1, y=2 表示式呼叫過程如下:

((λx.λy.2*x+y)1)2 = (λy.2+y) 2 = 4

從上面,我們可以看到,lambda 表示式的呼叫中,引數是有執行順序的,能感受到柯里化和組合的味道。

也就是說,由於函式就是表示式,表示式就是值。所以函式的返回值可以是一個函式,然後繼續進行呼叫執行,迴圈往復。

這樣,不同函式的層次問題也解決了,這裡用到了高階函式。在函數語言程式設計語言中,當函式是一等公民時,這個規律是生效的。

8、總結

說到這,大家從根本上對函數語言程式設計有了一個清晰的認知。比如它的數學基礎,為什麼存在、以及它和命令式語言的本質不同點。

lambda 演算系統 證明了:任何一個可計算函式都能用這種形式來表達和求值,它等價於圖靈機。

至此,我闡述了函式式語言出現的原因。以及支援函式式語言的重要理論支撐 —— lambda 演算系統的由來和基本內容。

函數語言程式設計為什麼要用函式去實現

上文提到過,運算系統的本質是運算。

函式只是封裝運算的一種手段,函式並不是真正的精髓,真正的精髓在於運算。

一、總結

說到這,大家從根本上對函數語言程式設計有了一個清晰的認知。比如它的數學基礎,為什麼存在、以及它和命令式語言的本質不同點。

二、函式式語言中,或者在函數語言程式設計中,函式二字的含義是什麼?它具備什麼能力?

1、函式二字的含義

這個函式是特指 滿足 lambda 演算的 lambda 表示式。函數語言程式設計中的函式表示式,又稱為 lambda 表示式。

該函式具有四個能力:

  1. 可以呼叫
  2. 是運算元
  3. 可以在函式內儲存資料
  4. 函式內的運算對函式外無副作用

2、運算元

在 JS 中,函式也是運算元,但它的運算只有呼叫。

3、函式內部儲存資料

閉包的存在使得函式內儲存資料得到了實現。函式執行,資料存在不同的閉包中,不會產生相互影響,就像面對物件中不同的例項擁有各自的自私有資料。多個例項之間不存在可共享的類成員。

4、總結

從這問可以知道,並不是一個語言支援函式,這個語言就可以叫做函式式語言,或者說就具有函數語言程式設計能力。

三、函數語言程式設計的特性關鍵詞有哪些?

大致列一下:

引用透明性、純潔性、無副作用、冪等性、惰性求值/非惰性求值、組合、柯里化、管道、高階性、閉包、不可變性、遞迴、partial monad 、 monadic 、 functor 、 applicative 、尾遞迴、嚴格求值/非嚴格求值、無限流和共遞迴、狀態轉移、 pointfree 、一等公民、隱式程式設計/顯式程式設計等。

1、引用透明性

定義:任何程式中符合引用透明的表示式都可以由它的結果所取代,而不改變該程式的含義。

意義:讓程式碼具有得到更好的推導性、可以直接轉成結果。

舉個例子:比如將 TS 轉換成 JS 的過程中,如果表示式具備引用透明性。那麼在編譯的時候,就可以提前把表示式的結果算出來,然後直接變成值,在 JS 執行的時候,執行的時間就會降低。

2、純潔性

定義:對於相同的輸入都將返回相同的輸出。

優點:

  1. 可測試
  2. 無副作用
  3. 可以並行程式碼
  4. 可以快取

3、惰性求值與非惰性求值

定義:如果一個引數是需要用到時,才會完成求值(或取值) ,那麼它就是惰性求值的。反之,就是非惰性求值。

(1)惰性求值:

true || console.log('原始碼終結者')

特點:當不再需要後續表示式的結果的時候,就終止後續的表示式執行,提高了速度,節約了資源。

(2)非惰性求值:

let i = 200
console.log(i+=20, i*=2, 'value: ' + i)
console.log(i)

特點:浪費 cpu 資源,會存在不確定性。

4、pointfree——隱式程式設計

函式無需要提及將要操作的資料是什麼。也就是說,函式不用指明操作的引數,而是讓組合它的函式來處理引數。

通常使用柯里和組合來實現 pointfree。

5、組合

(1)沒有組合的情況:

(2)組合後的情況:

具體的看我後面的實戰篇,我會通過例子來介紹組合的作用。

6、柯里化

7、函數語言程式設計的高階知識點——Functor 、Applicative 、Monad

點選圖片:Typescript版圖解Functor , Applicative 和 Monad

這些高階知識點,隨便一個都夠解釋很長的,這裡我就不做解釋了。我推薦一篇文章,闡述的非常透徹。

對於這三個高階知識點,我有些個人的看法。

  1. 第一個:不要被名詞嚇到,通過敲程式碼去感受其差異性。
  2. 第二個:既然要去理解函式式語言的高階知識,那就要儘可能的擺脫命令式語言的固有思想,然後再去理解這些高階知識點。
  3. 第三個:為什麼函數語言程式設計中,會有這些高階知識點?

關於第三個看法,我個人的感受就是:函數語言程式設計,需要你將隱式程式設計風格改成顯式風格。這也就意味著,你要花很多時間在函式的輸入和輸出上。

如何解決這個問題?

可以通過上述的高階知識點來完成,在特定的場景下,比如在 IO 中,不需要列出所有的可能性,只需要通過一個抽象過程來完成所有情況的處理,並保證不會丟擲異常。

它們都是為了一個目的,減少重複程式碼量,提高程式碼複用性。

8、總結

此問,我沒有詳細回答。我想說的是:

這些特性關鍵詞,都值得認真研究,這裡我只介紹了我認為該注意的點,具體的知識點,大家自行去了解和研究。

四、指令式程式設計和函數語言程式設計是對立的嗎?

從前面提到的一些闡述來看,指令式程式設計和函數語言程式設計不是對立的。它們既可以獨立存在,又可以共生。並且在共生的情況下,會發揮出更大的影響力。

我個人認為,在程式設計領域中,多正規化語言才是王道,單純只支援某一種正規化的程式語言是無法適應多場景的。

五、按照 FP 思想,不能使用迴圈,那我們該如何去解決?

對於純函式式語言,無法使用迴圈。我們能想到的,就是使用遞迴來實現迴圈,回顧一下前面提到的 lamda 演算系統,它是一套用於研究函式定義、函式應用和遞迴的系統。所以作為函式式語言,它已經做好了使用遞迴去完成一切迴圈操作的準備了。

六、丟擲異常會產生副作用,但如果不丟擲異常,又該用什麼替代呢?

說到這,我們需要轉變一下觀念:比如在命令式語言中,我們通常都是使用 try catch 這種來捕獲丟擲的異常。但是在純函式式語言中,是沒有 try catch 的,通常使用函子來代替 try catch 。

看到上面這些話,你可能會感到不能理解,為什麼要用函子來代替 try catch 。

其實有困惑是很正常的,主要原因就是:我們站在了命令式語言的理論基石上去理解函式式語言。

如果我們站在函式式語言的理論基石上去理解函式式語言,就不會感覺到困惑了。你會發現只能用遞迴實現迴圈、沒有 try catch 等要求,是合理且合適的。

PS: 這就好像是一直使用函式式語言的人突然接觸命令式語言,也會滿頭霧水的。

七、函數語言程式設計不允許使用可變狀態的嗎?如何沒有副作用的表達我們的程式?

可以使用區域性的可變狀態,只要該區域性變數不會影響外部,那就可以說改函式整體是沒有副作用的。

八、為什麼函數語言程式設計建議消滅掉語句?

因為語句的本質是:在於描述表示式求值的邏輯,或者輔助表示式求值。

JavaScript 函數語言程式設計的 5 問

一、為什麼函數語言程式設計要避免使用this

主要有以下兩點原因:

  1. JS 的 this 有多種含義,使用場景複雜。
  2. this 不取決於函式體內的程式碼。

    所有的資料都應以引數的形式提供給函式,而 this 不遵守這種規則。

二、為什麼JS函式內部可以使用for迴圈嗎?

很多人可能沒有想過這個問題

其實在純函式式語言中,是不存在迴圈語句的。迴圈語句需要使用遞迴實現,但是 JS 的遞迴效能並不好,比如沒有尾遞迴優化,那怎麼辦呢?

為了能支援函數語言程式設計,又要避免 JS 的遞迴效能問題。最後允許了函式內部可以使用 for 迴圈,你會看到 forEach 、 map 、 filter 、 reduce 的實現,都是對 for 迴圈進行了封裝。內部還是使用了 for 迴圈。

PS: 在 JS 中,只要函式內的 for 迴圈不影響外部,那就可以看成是體現了純潔性。

三、JS函式是一等公民是啥意識?這樣做的目的是啥?

我總結了一下,大概有以下意識:

  1. 能夠表達為匿名的直接量
  2. 能被變數儲存
  3. 能被其它資料結構儲存
  4. 有獨立而確定的名稱(如語法關鍵字)
  5. 可比較的
  6. 可作為引數傳遞
  7. 可作為函式結果值返回
  8. 在執行期可建立
  9. 能夠以序列化的形式表達
  10. 可(以自然語言的形式)讀的
  11. 可(以自然語言能在分佈的或執行中的程式中傳遞與儲存形式)讀的

1、序列化的形式表達

這個是什麼意識呢?

在 js 中,我們會發現有 eval 這個 api 。正是因為能夠支援以序列化的形式表達,才能做到通過 eval 來執行字串形式的函式。

2、總結

JS 之父設計函式為一等公民的初衷就是想讓 JS 語言可以支援函數語言程式設計。

函式是一等公民,就意味著函式能做值可以做的任何事情。

四、在JS中,如何做到函數語言程式設計?

核心思想:通過表示式消滅掉語句。

有以下幾個路徑:

  1. 通過表示式消滅分支語句 舉例:單個 if 語句,可以通過布林表示式消滅掉
  2. 通過函式遞迴消滅迴圈語句
  3. 用函式去代替值(函式只有返回的值在影響系統的運算,一個函式呼叫過程其實只相當於表示式運算中的一個求值)

五、用 JS 進行函數語言程式設計的缺點是什麼?

  • 缺少不可變資料結構( JS 除了原始型別,其他都是可變的)
  • 沒有提供一個原生的利於組合函式而產生新函式的方式,需要第三方支援
  • 不支援惰性序列
  • 缺少尾遞迴優化
  • JS 的函式不是真正純種函式式語言中的函式形式(比如 JS 函式中可以寫迴圈語句)
  • 表示式支援賦值

1、缺少尾遞迴優化

對於函數語言程式設計來說,缺少尾遞迴優化,是非常致命的。就目前而言,瀏覽器對尾遞迴優化的支援還不是很好。

什麼是尾遞迴?

如下圖所示: 

我們來看下面兩張圖:

第一張圖,沒有使用尾遞迴,因為 n * fatorial(n - 1) 是最後一個表示式,而 fatorial(n - 1) 不是最後一個表示式。第二張圖,使用了尾遞迴,最後一個表示式就是遞迴函式本身。

問題來了,為什麼說 JS 對尾遞迴支援的不好呢?

這裡我想強調的一點是,所有的直譯器語言,如果沒有解釋環境,也就是沒有 runtime ,那麼它就是一堆文字而已。JS 主要跑在瀏覽器中,需要瀏覽器提供解釋環境。如果瀏覽器的解釋環境對 JS 的尾遞迴優化的不好,那就說明,JS 的尾遞迴優化很差。由於瀏覽器有很多,可見 JS 要實現全面的尾遞迴優化,還有很長的路要走。

PS: 任何需求都是有優先順序的,對瀏覽器來說,像這種尾遞迴優化的優先順序,明顯不高。我個人認為,優先順序不高,是到現在極少有瀏覽器支援尾遞迴優化的原因。

參考

一、參考連結

  • 符號: 抽象、語義
  • Typescript版圖解Functor , Applicative 和 Monad
  • 邱奇-圖靈論題與lambda演算
  • 為什麼需要Monad?
  • 為什麼是Y?

二、參考書籍

  • JavaScript 函數語言程式設計指南
  • Scala 函數語言程式設計
  • Haskell 趣學指南
  • 其他電子書

未來,可期

本文通過闡述加提問的方式,對函數語言程式設計的一些理論知識進行了一次較為清晰的闡述。限於篇幅,一些細節無法展開,如有疑問,可以與我聯絡,一起交流一下,共同進步。

現在的前端,依舊在快速的發展中。從最近的 react Hooks 到 Vue 3.0 的 Function API 。我們能感受到,函數語言程式設計的影響力在慢慢變大。

在可見的未來,函數語言程式設計方面的知識,在腦海裡,是要有一個清晰的認知框架。

最後,發表下我個人的看法:

JavaScript 最終會回到以函式的形式去處理絕大多數事情的模式上。

更多內容敬請關注 vivo 網際網路技術 微信公眾號

注:轉載文章請先與微訊號:labs2020 聯絡。

相關文章