面向牆外編程系列001:GOTOCON : 函數式編程 made simple程序員
👨💻 Conference : GOTOCON 2018數據庫
👤 Speaker : Russ Olsen[1]express
🔗 Original Link : GOTO 2018 • Functional Programming in 40 Minutes • Russ Olsen[2]編程
🏷 Tags : 函數式編程數組
函數式編程已經不是一個新概念了,最先能夠溯源到上世紀五十年代的 Lambda calculus[3] 和 Lisp 。儘管如此,人們對函數式編程存在必定誤解:學術性質太強,而且很是複雜。實際上並不是如此,你將會看到函數式編程的思想其實很是簡單。安全
學習函數式編程須要丟棄全部你所知道的編程思想嗎?這種說法部分正確,由於函數式編程思想的確很不同。可是這等因而把函數式編程過分神化了,函數式編程和麪向對象同樣,都只是一種爲了下降代碼複雜度的編程模式。面向對象很成功,可是代碼依舊很複雜,函數式編程是一種徹底不一樣的 approach,也許能作得更好。bash
先來梳理一下咱們熟知的構成程序的概念有哪些:微信
難道咱們要把這些概念也丟棄嗎?顯然不能,因此,學習函數式編程不是從零開始,把它看做是對已有編程概念的 重構 更加恰當。數據結構
與其說學習函數式編程要 「Forget everything you know about programming」,不如說要 「Refactor everything you know about programming」。併發
假設你在開發下面這個程序。
最開始程序 works ,可是隨着時間發展,需求變動,你須要添加新的模塊、刪除某些不須要了的模塊,還須要在不一樣模塊之間修改依賴關係。 一段時間後,it works ,but in a mess。
如今坐下來,從一張白紙開始,從新重構應用:
可是有趣的是,當咱們要重構一個系統的時候,並不會真的從頭開始,重寫全部代碼。咱們作的是,把以前 works 的組件抽出來,從新組織。
重構事後,你加了一個 message bus,來串聯不一樣組件之間的通訊。
面向對象編程語言處處是下面這種的定義和限制:
During the type erasure process, the Java compiler erases all type parameters and replaces each with its first bound if the type parameter is bounded, or Objectif the type parameter is unbounded.
Protected methods: a protected method is similar to a private method, with the addition that it can be called with, or without, an explicit receiver, but that receiver is alwaysself(it’s defining class)or an object that inherits fromself(ex: is_a?(self)).
A friend function of a class is defined outside that class’ scope but it has the right to access all private and protected members of the class. Even though the prototypes for friend functions appear in the class definition, friends are not member functions.
其種類繁多,你仔細想過沒有,這是否是讓問題自己過於複雜了?咱們須要記住這麼多限制嗎?也許咱們須要換一種方式、重構一下? 那就讓咱們開始吧!
仍是從一張白紙開始:
把要保留的編程概念加進來:
該如何把底層的編程語言基礎,和頂層的程序組織起來呢?讓我慢慢來看。
也許數學家們可以幫咱們的忙。數學和計算機科學在一個方面很像:抽象,試圖從最基本的元素開始,抽象出上層概念。數學家寫了一本叫《Principia Mathematica》的書,從很是很是簡單的公理開始,推導出了整個數學大廈,光是證實 1 + 1 = 2 就花了 300 多頁。。。
那麼咱們能從數學家那裏借來什麼概念呢?沒錯,函數!
數學意義上的函數和咱們程序中的函數不同,函數就是一個集合到另外一個集合的映射:給我一個輸入,我還你一個輸出,不會對外界作任何操做。並且更重要的是,對於任何特定的輸入,我還給你的輸出必定是同樣的。
而編程語言中函數的概念不太同樣,編程語言中的函數能夠刪除文件、插入數據庫,而這些屬於反作用。並且,編程語言的函數給定一個輸入,輸出結果可能會不同,好比輸出依賴於當前時間的狀況。
若是咱們想借鑑數學中的函數這個概念,就須要給編程語言中的函數強加一些特定的規則,讓他表現得和數學函數同樣。(咱們把知足這種條件的函數叫作純函數。)這些規則很簡單:
咱們這樣作,並非上層設計,通過嚴格的證實,認定純函數必定能讓咱們的代碼更簡單,這只是一種指望。 就像面向對象同樣,咱們抽象出現實世界一一對應的類和對象,這並無嚴格的證實能夠保證這樣作就是對的,只是咱們指望這樣作能夠。
咱們須要問兩個問題:
When asked, 「What are the advantages of writing in a language without side effects?,」 Simon Peyton Jones, co-creator of Haskell, replied, 「You only have to reason about values and not about state. If you give a function the same input, it’ll give you the same output, every time. This has implications for reasoning, for compiling, for parallelism.」 — From the book, Masterminds of Programming[4]
如前面所說,純函數核心在於對於特定輸入,永遠有同樣的輸出,且沒有反作用。有了純函數,你就只須要考慮 values 了,不用管 state了(相信你也據說或經歷過共享變量帶來的痛苦)。 這使得函數式編程具有如下幾個優點:
純函數不會說謊,你能從函數定義獲得幾乎全部信息。 純函數更容易組合
這和 unix 的 pipeline 很像。
val x = doThis(a).thenThis(b)
.andThenThis(c)
.doThisToo(d)
.andFinallyThis(e)
複製代碼
顯而易見,純函數只依賴於輸入,不須要考慮其餘複雜的狀態。
「If there is no data dependency between two pure expressions, then their order can be reversed, or they can be performed in parallel and they cannot interfere with one another (in other terms, the evaluation of any pure expression is thread-safe).」
只要兩個純函數之間沒有數據依賴,他們的執行順序就能夠任意替換,或者能安全地併發執行,換句話說, pure expression 是線程安全的。《七週七併發模型》一書中也將函數式編程看成一種重要的併發模型,甚至這可能就是將來的趨勢,由於這種方式最簡單,你徹底不用考慮鎖的問題。 想要了解純函數更多的優點,推薦看這篇文章:The Benefits of Pure Functions | alvinalexander.com
咱們回答完了第一個問題,知道了純函數是有優點的,如今咱們的藍圖上添加了第一把武器:純函數。
接下來回答另外一個問題:如何在語言層面確保純函數可以實現?
咱們都知道,通常的數據結構是可變的,好比一個數組,沒有任何限制阻止你修改其中的某個元素:
可是這樣一來,就違背了咱們前面說的規則:不會產生反作用。
有一個解決方案:讓全部的數據結構都不可變。一旦你建立了這個列表,你不能修改它;一旦你建立了這個 hashmap,你不能修改它。固然,函數確定是須要對輸入作一系列操做(計算)的,不然這個函數有何意義。只不過操做的方式不同: 好比有一個 array :x = ["a","b","c"],在一個純函數內要執行 x[1] = "Q",這會先將 x 複製一份,把複製品的第二個元素修改成 "Q",原來的 x 沒有變化。(copy on modification)
如今咱們不用擔憂調用的函數會在咱們不注意的狀況下修改傳入的值了,不過這會帶來性能問題,由於拷貝太多了!所幸有人很聰明,想出了Persistent Data Structure,使得拷貝的次數儘量達到最低。 好比,一個數組,在底層多是一個樹形結構。
要修改的時候,只會修改樹的其中一部分,其餘部分是和原來的元素共用的。Persistent Data Structure 的原理這裏就不細講了,有興趣的能夠自行了解。
到目前爲止,咱們獲得了第二把武器:Persistent Data Structure.
而後再來仔細想一想什麼是 side effect。修改文件、插入數據庫、調用外部服務,這些對於咱們程序員來講屬於 side effects。可是對於用戶來講,這就是他們所指望完成的操做!一個報表應用的使用者只關心數據庫裏面的數據、生成的報表,他們纔不關心代碼,也不懂什麼函數式編程。若是一個應用徹底沒有「反作用」,那這個應用也就沒有存在的意義。
Side effects are what we paid to do !
因此咱們須要在「函數式編程理想世界」 和 「充滿 side effects 的複雜多變的現實世界」之間架起一座橋樑。
其中一個現實世界的的問題:如何實現可變狀態?好比一個網站的訪問次數計數器,這就是一個可變狀態。但是在純粹的函數式編程世界裏,數據都是不可變的,Clojure的解決方案,是 Atoms。能夠把 Atoms 看做是包含可變狀態的容器。
修改一個 Atoms 的方式是:丟給它一個函數,atoms 接收到這個函數,把 atoms 當前的 value 傳遞給函數,函數根據這個 value 計算產生新的 value,返回給 atoms,atoms再將此 value 做爲新的 value 保存下來。
Atoms 的強大之處還體如今,當有兩個函數同時到達時:
fx 和 gx 兩個函數(假設都是加一函數)同時傳遞到 atoms 面前,他們看到的值都是 59,通過計算,兩個函數產生的結果都是60。可是,老是會有一個函數(好比說是 fx )先把 59 更新爲 60。另外一個函數(gx)再次把值更新爲 60 的時候,atoms 就會檢測到底層的數據已經發生了變化,而後讓 gx 再執行一次!
因爲沒有 side effects,因此重試是沒有任何問題的。這屬於 atoms 的衝突檢測機制。 可變狀態的問題解決了,那麼更新數據庫、操做文件這些操做該怎麼辦呢?Clojure 有 Agents。
簡單來講,Agents的做用是接受帶有 side effects(好比進行數據庫操做)的函數,加入到一個隊列裏面,依次執行。 如今咱們有了第三把武器:鏈接外界的橋樑。
而這,也就是咱們設計的函數式編程的最終藍圖了。是否是很簡單,並無想象的那麼複雜。
函數式編程並不是萬能解藥,它不能徹底消除重複代碼,也不能阻止你寫出垃圾代碼。
函數式編程經過引入純函數,具有了面向對象編程所沒有的優點,能讓代碼更簡單,更容易理解。而持久性數據結構使得純函數變得可能。將來是一個多核時代,函數式編程語言天生線程安全(由於沒有可變數據結構,一個線程不可能修改另外一個線程的數據),很是適合在多核環境運行。
函數式編程編寫的函數沒有反作用,可是應用的使命自己就是反作用,要鏈接 「理想的函數式編程世界」 和 「複雜的充滿反作用的現實世界」,能夠用 atoms 或者 agents 這類橋樑。
面向對象依舊是當前應用最普遍的編程思想,也構建了無數大型項目,而函數式編程相比更爲簡單,將來前景確定會愈來愈好。
[1] Russ Olsen: russolsen.com/
[2] GOTO 2018 • Functional Programming in 40 Minutes • Russ Olsen: www.youtube.com/watch?v=0if…
[3] Lambda calculus: en.wikipedia.org/wiki/Lambda…
[4] Masterminds of Programming: amzn.to/2bedXb4
[5] The Benefits of Pure Functions | alvinalexander.com: alvinalexander.com/scala/fp-bo…
歡迎關注我的微信公衆號:面向牆外編程。帶你得到更多高質量的牆外編程資源。