祝:我和個人祖國,固然還有各位小夥伴,國慶快樂哈!國慶 buff
加持, bug
退散。html
話說我已經四個月沒有更新文章了😂,有一些客觀因素,不過我又回來啦️。前端
由於我以前寫過一篇文章:react
現在我再看這篇文章的時候,發現有些觀點並不深刻,沒有那種直接本質的穿透感。因而,我想從新對函數式編程的理論篇進行一次高緯度的歸納。程序員
在本文中,我將經過背景加提問的方式,對函數式編程的本質、目的、前因後果等方面進行一次清晰的闡述,請和我一塊兒往下看。github
經過對計算機和編程語言發展史的闡述,找到函數式編程的時代背景。經過對與函數式編程強相關的人物介紹,來探尋和感覺函數式編程的那些鮮爲人知的本質。編程
本篇文章首發於 vivo 互聯網技術 微信公衆號上:api
做者:楊昆。固然就是我本人啦😂,掘金版更逗一些。緩存
下面,我列一個簡要目錄:
lambda
演算系統是什麼?lambda
具體說的是啥內容?lambda
和函數有啥聯繫?爲啥會有 lambda
演算系統?FP
思想,不能使用循環,那咱們該如何去解決?JavaScript
函數式編程的 5 問this
JavaScript
中函數是一等公民, 就能夠得出 JavaScript
是函數式語言嗎?爲何說 JS
是多態語言?JS
函數內部可使用 for
循環嗎?JS
函數是一等公民,是什麼意識?這樣作的目的是啥?JS
進行函數式編程的缺點是什麼?簡要目錄介紹完啦,你們請和我一塊兒往下看。
PS:我好像是一個在海邊玩耍的孩子,不時爲拾到比一般更光滑的石子,或更美麗的貝殼而歡欣鼓舞,而展示在我面前的是徹底未探明的的真理之海。
計算機和編程語言的發展史是由人類主導的,去了解在這個過程當中起到關鍵做用的人物是很是重要的。
下面咱們一塊兒來認識幾位起關鍵做用的超巨。
點擊
TP
介紹: 戴維·希爾伯特
希爾伯特 被稱爲 數學界的無冕之王 ,他是 天才中的天才。
在我看來,希爾伯特最厲害的一點就是:
他鼓舞你們去將證實過程純機械化,由於這樣,機器就能夠經過形式語言推理出大量定理。
也正是他的堅持推進,形式語言才逐漸走向歷史的舞臺中央。
點擊
TP
介紹: 艾倫·麥席森·圖靈
艾倫·麥席森·圖靈 稱爲 計算機科學之父。
我認爲,他最偉大的成就,就是發明了圖靈機:
上圖所示,就是圖靈機的模型圖。
這裏咱們注意一點:
從圖中,咱們會發現,每一個小方格可存儲一個數字或者字母。這個信息很是重要,你們能夠思考一下。
PS:
等我介紹 馮·諾依曼 的時候,就會明白它們之間的聯繫。
點擊
TP
介紹: 阿隆佐·邱奇
阿隆佐·邱奇,艾倫·麥席森·圖靈 的博導。
他最偉大的成就,就是:
發明了 λ(lambda) 演算。
如上圖,就是 λ(lambda) 演算
的基本形式。
阿隆佐·邱奇 發明的
λ演算
和圖靈發明的圖靈機,一塊兒改寫了當今世界,形式語言的歷史。
思考: 邱奇的 λ演算
和圖靈的圖靈機,這二者有什麼區別和聯繫?
點擊
TP
介紹: 馮·諾依曼
馮·諾依曼 被稱爲 計算機之父。
他提出了 馮·諾依曼 體系結構:
從上圖,咱們能夠看出:
馮·諾依曼 體系結構由運算器、控制器、存儲器、輸入設備、輸出設備五個部分組分組成。採用二進制邏輯,程序存儲、執行做爲計算機制造的三個原則。
注意一個信息:
咱們知道,計算機底層指令都是由 0
和 1
組成,經過對 0
和 1
的 CRUD
,來完成各類計算操做。咱們再看圖靈機,會發現其每一個小方格可存儲一個數字或者字母。
看到這,是否是發現馮·諾依曼體系結構和圖靈機有一些聯繫。
是的,現馮·諾依曼體系結構就是按照圖靈機的模型來實現的計算機結構。計算機的 0
和 1
,就是圖靈機的小方格中的數字或者字母的特例。
由於若是想完全解開函數式編程的困惑,那就必需要去了解這時代背景和關鍵人物的一些事蹟。
邱奇是圖靈的博士生導師,他們之間有一個著名的論題,那就是 邱奇-圖靈論題 。
論題大體的內容是:
圖靈和 lambda 這兩種模型,有沒有一個模型能表示的計算,另外一個模型表示不了呢?
到目前爲止,這個論題尚未答案。也正由於如此,讓不少人對 lambda
模型充滿了信心。後面的歲月中,lambda
模型一直在被不少人研究、論證、實踐。
它叫 ENAIC。
1946
年,世界上第一臺電子計算機—— ENIAC
問世,它能夠改變計算方式,便可以更改程序。
也就是說:
它是一臺可編程計算機。
perl
語言的設計者 Larry Wall
說過:
優秀的程序員具備三大美德:懶惰、急躁、傲慢。
可編程完美詮釋了懶惰的美德。在
ENAIC
誕生後,出現了各類各樣的 程序設計語言。三大美德也體現的淋漓盡致。
從上圖能夠得到如下信息:
HTML
、XML
是數據設計語言。MySQL
。 就是邏輯式語言,它經過提問的方式來完成操做。這個分類能夠好好看看,會有一些感覺的。
上圖很是簡單明瞭,只到 1995
年。
時間線大概是這樣的:
xxx ---> xxx ---> .... ---> JavaScript ...
時間來到了 1996
年,JavaScript
誕生了!
圖中這位老哥叫 布蘭登·艾奇 。那一年,他34歲。
從上圖中你會有以下幾點感覺:
Java
一點興趣也沒有。Scheme
語言,將函數提高到一等公民的地位,讓 JS
擁有了函數式編程的能力。埋下了 JS
能夠進行函數式編程的種子。JS
是既能夠函數式編程,也能夠面對對象編程。我在回顧程序設計語言的發展史和一些故過後,我並不認爲 JavaScript
是一個爛語言,相反正是這種中庸之道,才使得 JavaScript
可以流行到如今。
經過對計算機語言的發展史和關鍵人物的簡潔介紹,咱們能夠從高層面去體會到函數式編程在計算機語言發展史中的潛力和影響力。
不過,經過背景和人物的介紹,對函數式編程的理解仍是有限的。下面我將經過提問的方式來闡述函數式編程的前因後果。
下面將經過 10
個問題的解答,來闡述函數式編程的理論支撐、函數式編程的誕生背景、函數式編程的核心理論以及推導等知識。
函數式語言的存在,是爲了實現運算系統的本質——運算。
重要的事情重複三遍:運算、運算、運算。
計算機未問世以前,四位大佬 阿蘭·圖靈、約翰 ·馮·諾依曼 、庫爾特 ·哥德爾 和阿隆左 ·丘奇。展開了對形式化的運算系統的研究。
經過形式系統來證實一個命題:
能夠用簡單的數學法則表達現實系統。
從上文的圖片和解析可知,圖靈機和馮諾依曼體系的計算機系統都依賴存儲(內存)進行運算。
換句話說就是:
經過修改內存來反映運算的結果。並非真正意義上的運算。
修改內存並非咱們想要的,咱們想要的僅僅是運算。從目的性的角度看,修改內存能夠說是運算系統中的反作用。或者說,是表現運算結果的一種手段。
這一切,圖靈的博導邱奇看在眼裏,他看到了問題的本質。爲了實現運算系統的本質——運算,即不修改內存,直接經過運算拿到結果。
他提出了 lambda
演算的形式系統,一種更接近於運算纔是本質的理論。
從語言學分類來講:
是兩種不一樣類型的計算範型。
從硬件系統來講:
它們依賴於各自不一樣的計算機系統(也就是硬件)。 爲何依賴不一樣的硬件,是由於若是用馮諾依曼結構的計算機,就意味着要靠修改內存來實現運算。可是,這和 lambda
演算系統是相矛盾的。
由於基於 lambda
演算系統實現的函數式語言,是不須要寄存器的,也不存在須要使用寄存器去存儲變量的狀態。它只注重運算,運算結束,結果就會出來。
最大的隔閡就是依賴各自不一樣的計算機系統 。
目前爲止,在技術上作不到基於 A
範型的計算機系統,同時支持 B
範型。也就是說,不能期望在 X86
指令集中出現適用於 lambda
演算 的指令、邏輯或者物理設計。
你可能會疑問,既然硬件不支持,那咱們爲何還能進行函數式編程?
其實現實中,大多數人都是用的馮諾依曼體系的命令式語言。因此爲了得到特別的計算能力和編程特性。語言就在邏輯層虛擬一個環境,也由於這樣,誕生了 JS
這樣的多範型語言,以及 PY
這種腳本語言。
究其根源,是由於,馮·諾依曼體系的計算機系統是基於存儲與指令系統的,並非基於運算的。
在當時硬件設備條件的限制下,邱奇提出的 lambda
演算,在很長時間內,都沒有被程序設計語言所實現。
直到馮諾依曼等人完成了 EDVAC
的十年以後。一位 MIT
的教授 John McCarthy
對邱奇的工做產生了興趣。在 1958
年,他公開了表處理語言 LISP
。這個 LISP
語言就是對邱奇的 lambda
演算的實現。
自此,世界上第一個函數式語言誕生了。
LISP
就是函數式語言的鼻祖,完成了 lamda
演算的實現,實現了 運算纔是本質的運算系統。
上圖是 Lisp
的圖片,感覺一下圖片中符號的魅力。
爲何我說是曙光?
是由於,並無真正的勝利。此時的 LISP
依舊是工做在馮·諾依曼計算機上,由於當時只有這樣的計算機系統。
因此從
LISP
開始,函數式語言就是運行在解釋環境而非編譯環境中的。也就是傳說中的腳本語言,解釋器語言。
直到 1973
年,MIT
人工智能實驗室的一組程序員開發了,被稱爲 LISP
機器的硬件。自此,阿隆左·丘奇的 lambda
演算終於獲得了 硬件實現。終於有一個計算機(硬件)系統能夠宣稱在機器指令級別上支持了函數式語言。
關於這問,我闡述了不少,從函數式語言誕生的目的、到函數式語言誕生的艱難過程、再到計算機硬件的限制。最後在不斷的努力下,作到了既能夠經過解釋器,完成基於馮·諾依曼體系下,計算機系統的函數式編程。也能夠在機器指令級別上支持了函數式語言的計算機上進行純正的函數式編程。
思考題:想想,在現在,函數式編程爲何愈來愈被人所瞭解和掌握。
lambda
是一種解決數學中的函數語義不清晰,很難表達清楚函數的結構層次的問題的運算方案。
也就是在運算過程當中,不使用函數中的函數運算形式,而使用
lambda
的運算形式來進行運算。
lambda
運算而產生的運算範型。lambda
演算系統是學習函數式編程的一個很是重要的知識點。它是整個函數式編程的理論基石。
以下圖所示:
從上面的數學函數中,咱們能夠發現如下幾點:
x2-2*x+1
既能夠當作是函數 f(x)
的定義,又能夠當作是函數 g(x)
對變量 x-1
的調用。體會上面幾點,咱們會發現:
數學中的函數語義並不清晰,它很難表達清楚函數的結構層次。對此,邱奇給出瞭解決方法,他提出了 lambda(λ)
演算。
基本定義形式:
λ<變量>.<表達式>
經過這種方法定義的函數就叫 λ(lambda)
表達式。
咱們能夠把 lambda
翻譯成函數,便可以把 lambda
表達式念成函數表達式。
PS: 這裏說一下,函數式語言中的函數,是指 lambda(函數),它和咱們如今的通用語言中,好比
C
中 的function
是不一樣的兩個東西。
λ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
λx.λy.x+y
x=1, y=2,
調用過程以下
((λx.λy.2*x+y)1) 2 = (λy.2+y) 2 = 4
能夠看到 ,函數的返回值也能夠是一個函數,這樣不一樣函數的層次問題也解決了,這裏用到了高階函數。在函數式編程語言中,當函數是一等公民時,這個規律是生效的
仔細想一下,你會發現 lambda
演算系統,簡潔而又優美。它能夠被稱爲最小的通用程序設計語言。λ
演算系統的構建過程,就是一個程序語言從無到有的構建過程。整個系統中任何一個複雜的 λ
表達式都是由:λ
抽象、應用和括號,粘合在一塊兒的。
lambda
演算系統 證實了:
任何一個可計算函數都能用這種形式來表達和求值,它等價於圖靈機。
至此,我闡述了函數式語言出現的緣由。以及支持函數式語言的重要理論支撐 —— lambda
演算系統的由來和基本內容。
上文提到過,運算系統的本質是運算。
函數只是封裝運算的一種手段,函數並非真正的精髓,真正的精髓在於運算。
說到這,你們從根本上對函數式編程有了一個清晰的認知。好比它的數學基礎,爲何存在、以及它和命令式語言的本質不一樣點。
這個函數是特指 知足 lambda
演算的 lambda
表達式。函數式編程中的函數表達式,又稱爲 lambda
表達式。
該函數具備四個能力:
在 JS
中,函數也是運算元,但它的運算只有調用。
閉包的存在使得函數內保存數據獲得了實現。函數執行,數據存在不一樣的閉包中,不會產生相互影響,就像面對對象中不一樣的實例擁有各自的自私有數據。多個實例之間不存在可共享的類成員。
從這問能夠知道,並非一個語言支持函數,這個語言就能夠叫作函數式語言,或者說就具備函數式編程能力。
大體列一下:
引用透明性、純潔性、無反作用、冪等性、惰性求值/非惰性求值、組合、柯里化、管道、高階性、閉包、不可變性、遞歸、partial
monad
、 monadic
、 functor
、 applicative
、尾遞歸、嚴格求值/非嚴格求值、無限流和共遞歸、狀態轉移、 pointfree
、一等公民、隱式編程/顯式編程等。
定義:
任何程序中符合引用透明的表達式均可以由它的結果所取代,而不改變該程序的含義。
意義:
讓代碼具備獲得更好的推導性、能夠直接轉成結果。
舉個例子:
好比將 TS
轉換成 JS
的過程當中,若是表達式具有引用透明性。那麼在編譯的時候,就能夠提早把表達式的結果算出來,而後直接變成值,在 JS
運行的時候,執行的時間就會下降。
定義:
對於相同的輸入都將返回相同的輸出。
優勢:
定義:
若是一個參數是須要用到時,纔會完成求值(或取值) ,那麼它就是惰性求值的。反之,就是非惰性求值。
惰性求值:
true || console.log('源碼終結者')
複製代碼
特色:當再也不須要後續表達式的結果的時候,就終止後續的表達式執行,提升了速度,節約了資源。
非惰性求值:
let i = 100
console.log(i+=20, i*=2, 'value: '+i)
console.log(i)
複製代碼
特色:浪費 cpu 資源,會存在不肯定性。
函數無須要說起將要操做的數據是什麼。也就是說,函數不用指明操做的參數,而是讓組合它的函數來處理參數。
一般使用柯里和組合來實現
pointfree
這些高級知識點,隨便一個都夠解釋很長的,這裏我就不作解釋了。我推薦一篇文章,闡述的很是透徹。
對於這三個高級知識點,我有些我的的見解。
第一個:不要被名詞嚇到,經過敲代碼去感覺其差別性。
第二個:既然要去理解函數式語言的高級知識,那就要儘量的擺脫命令式語言的固有思想,而後再去理解這些高級知識點。
第三個:爲何函數式編程中,會有這些高級知識點?
關於第三個見解,我我的的感覺就是:
函數式編程,須要你將隱式編程風格改爲顯式風格。這也就意味着,你要花不少時間在函數的輸入和輸出上。
如何解決這個問題?
能夠經過上述的高級知識點來完成,在特定的場景下,好比在 IO
中,不須要列出全部的可能性,只須要經過一個抽象過程來完成全部狀況的處理,並保證不會拋出異常。
咱們能夠把這些函數式編程的高級知識點和麪對對象編程的繼承等作比較,會發現:
它們都是爲了一個目的,減小重複代碼量,提升代碼複用性。
此問,我沒有詳細回答,我想說的是:
這些特性關鍵詞,都值得認真研究,這裏我只介紹了我認爲該注意的點,具體的知識點,你們自行去了解和研究。
從前面提到的一些闡述來看,命令式編程和函數式編程不是對立的。它們既能夠獨立存在,又能夠共生。而且在共生的狀況下,會發揮出更大的影響力。
我我的認爲,在編程領域中,多範式語言纔是王道,單純只支持某一種範式的編程語言是沒法適應多場景的。
對於純函數式語言,沒法使用循環。咱們能想到的,就是使用遞歸來實現循環,回顧一下前面提到的 lamda
演算系統,它是一套用於研究函數定義、函數應用和遞歸的系統。因此做爲函數式語言,它已經作好了使用遞歸去完成一切循環操做的準備了。
說到這,咱們須要轉變一下觀念:
好比在命令式語言中,咱們一般都是使用 try catch
這種來捕獲拋出的異常。可是在純函數式語言中,是沒有 try catch
的,一般使用函子來代替 try catch
。
看到上面這些話,你可能會感到不能理解,爲何要用函子來代替 try catch
。
其實有困惑是很正常的,主要緣由就是:
咱們站在了命令式語言的理論基石上去理解函數式語言。
若是咱們站在函數式語言的理論基石上去理解函數式語言,就不會感受到困惑了。你會發現只能用遞歸實現循環、沒有 try catch
等要求,是合理且合適的。
PS: 這就好像是一直使用函數式語言的人忽然接觸命令式語言,也會滿頭霧水的。
可使用局部的可變狀態,只要該局部變量不會影響外部,那就能夠說改函數總體是沒有反作用的。
由於語句的本質是:
在於描述表達式求值的邏輯,或者輔助表達式求值。
JavaScript
函數式編程的 5 問主要有如下兩點緣由:
JS
的 this
有多種含義,使用場景複雜。this
不取決於函數體內的代碼。this
不遵照這種規則。不少人可能沒有想過這個問題
其實在純函數式語言中,是不存在循環語句的。循環語句須要使用遞歸實現,可是 JS
的遞歸性能並很差,好比沒有尾遞歸優化,那怎麼辦呢?
爲了能支持函數式編程,又要避免 JS
的遞歸性能問題。最後容許了函數內部可使用 for
循環,你會看到 forEach
、 map
、 filter
、 reduce
的實現,都是對 for
循環進行了封裝。內部仍是使用了 for
循環。
PS: 在
JS
中,只要函數內的for
循環不影響外部,那就能夠當作是體現了純潔性。
我總結了一下,大概有如下意識:
這個是什麼意識呢?
在 js
中,咱們會發現有 eval
這個 api
。正是由於可以支持以序列化的形式表達,才能作到經過 eval
來執行字符串形式的函數。
JS
之父設計函數爲一等公民的初衷就是想讓JS
語言能夠支持函數式編程。
函數是一等公民,就意味着函數能作值能夠作的任何事情。
核心思想:經過表達式消滅掉語句。
有如下幾個路徑:
if
語句,能夠經過布爾表達式消滅掉JS
除了原始類型,其餘都是可變的)JS
的函數不是真正純種函數式語言中的函數形式(好比 JS
函數中能夠寫循環語句)對於函數式編程來講,缺乏尾遞歸優化,是很是致命的。就目前而言,瀏覽器對尾遞歸優化的支持還不是很好。
什麼是尾遞歸?
以下圖所示:
咱們來看下面兩張圖
第一張圖,沒有使用尾遞歸,由於 n * fatorial(n - 1)
是最後一個表達式,而 fatorial(n - 1)
不是最後一個表達式。第二張圖,使用了尾遞歸,最後一個表達式就是遞歸函數自己。
問題來了,爲何說
JS
對尾遞歸支持的很差呢?
這裏我想強調的一點是,全部的解釋器語言,若是沒有解釋環境,也就是沒有 runtime
,那麼它就是一堆文本而已。JS
主要跑在瀏覽器中,須要瀏覽器提供解釋環境。若是瀏覽器的解釋環境對 JS
的尾遞歸優化的很差,那就說明,JS
的尾遞歸優化不好。因爲瀏覽器有不少,可見 JS
要實現全面的尾遞歸優化,還有很長的路要走。
PS: 任何需求都是有優先級的,對瀏覽器來講,像這種尾遞歸優化的優先級,明顯不高。我我的認爲,優先級不高,是到如今極少有瀏覽器支持尾遞歸優化的緣由。
本文經過闡述加提問的方式,對函數式編程的一些理論知識進行了一次較爲清晰的闡述。限於篇幅,一些細節沒法展開,若有疑問,能夠與我聯繫,一塊兒交流一下,共同進步。
如今的前端,依舊在快速的發展中。從最近的 react Hooks
到 Vue
3.0
的 Function API
。咱們能感覺到,函數式編程的影響力在慢慢變大。
在可見的將來,函數式編程方面的知識,在腦海裏,是要有一個清晰的認知框架。
最後,發表下我我的的見解:
JavaScript 最終會回到以函數的形式去處理絕大多數事情的模式上。
能夠關注個人掘金博客或者 github
來獲取後續的系列文章更新通知。
掘金系列技術文章彙總以下,以爲不錯的話,點個 star 鼓勵一下哦。
我是源碼終結者,歡迎技術交流。
也能夠進 前端狂想錄羣-炫舞羣 你們一塊兒頭腦風暴。
啥也別說了,直接上圖:
祝各位首富喜提國慶 buff
,遠離 bug
,開心過完七天小長假哦。
最後:尊重原創,轉載請註明出處哈😋