JavaScript函數語言程式設計之為什麼要函數語言程式設計(非嚴謹技術層面的扯淡)

磚用冰西瓜發表於2018-06-28

我的github github.com/zhuanyongxi…

這可能是一篇會被經常改動的文章,它記錄了現在的我對函數語言程式設計粗淺的理解。

函數語言程式設計並不是github上面的一個工具庫,它的年齡比JavaScript要大得多,它是一種經過了幾十年,被眾多電腦科學家證明了的行之有效的程式設計正規化。它不是學會了幾個函數語言程式設計工具的API就能完全掌握的,它更多的是程式設計思維上的轉變。具體一點的程式碼例項可以看《JavaScript函數語言程式設計之pointfree與宣告式程式設計》

那到底為什麼用使用函數語言程式設計

如果總結成一個字,那就是為了“爽”。“爽”在哪?讀和寫

這可能會讓你不以為然,讀和寫對你來說可能不是最重要的事情。不過以我當前的認知水平來看,設計模式、程式設計正規化,甚至是程式語言本身,他們的最大意義都在於這兩個字。

程式碼是給人看的

比如程式語言,它的落點在於“語言”,“語言”是用來幹什麼的?交流溝通。跟誰交流溝通?跟人,而非機器。我們寫的程式碼會經過層層編譯,最終轉化成0和1才能被計算機執行。那我們為什麼要這麼麻煩,而不直接用0和1程式設計?因為不好寫也不好讀。如果你手速足夠快,大腦也足夠發達,用0和1程式設計也是可以的。可那樣的話,還需要計算機嗎?雖然層層編譯的過程,勢必會降低效率,不過有摩爾定律來接盤,大可不必擔心。

函式式的“好讀”體現在哪裡?

如果全部功能的實現只需要幾行程式碼,大談設計模式、程式設計正規化就太矯情了。我們之所以需要這些東西,是因為隨著程式碼量的增多,程式碼的讀寫對我們造成了困擾。

那函數語言程式設計是如何解決這些問題的?

比如純函式,它有很多優點,個人認為最重要的是:它明顯的降低我們的認知負荷。

從最普通的函式說起:

var a = 1;
function A(val) {
  a = 2;
  return a + val;
} 
複製程式碼

這裡的變數a就是被大家嗤之以鼻的全域性變數。它非常容易導致出現bug,當程式碼量很多的時候,很可能會因為重名而被覆蓋,從而導致其他使用了這個變數的程式出錯。即便是正常的修改,當我們再次使用函式A的時候,弄清變數a當前的狀態也是十分的困難,這就使得函式A變得難以理解,難以理解的函式就得不到使用者的信任

物件導向程式設計的“封裝”在一定程度上解決了這個問題:

var obj = {
  a: 1,
  A: function(val) {
    return this.a + val;
  }
}
複製程式碼

封裝了之後全域性變數大大的減少了,即便有兩個物件重名了,相對上面的情況,修改起來也不會有那麼大的負擔。但它並沒有徹底解決這一問題,因為屬性a是可變的,每次呼叫方法A的時候,弄清屬性a的狀態依然不容易。

而函數語言程式設計不存在這個問題。在函數語言程式設計中,使用可變的外部的變數是不被允許的。例如:

var a = 1;	// 除了當做引數傳入,使用變數a的函式都不是純函式
function A(val) {	// 不是純函式
  return a + val;
}


const b = 1;
const B = function(val) {	// 純函式
  return b + val;
}
複製程式碼

隨著程式碼量逐漸增多,這種寫法的優勢就愈發明顯了,如果還能更進一步的減少耦合,那你就可以在很大程度上去“斷章取義”,不去管上下文。每一個純函式我們都可以充分信任,它的輸出只與傳進去的引數有關。

不僅僅在函數語言程式設計中,物件導向的程式設計通常也會建議寫純函式。

純函式對程式碼的除錯也有極大的好處,這也是函數語言程式設計的優點之一:方便除錯。

在初學函數語言程式設計的時候,會很難理解這一點,因為當我們使用純函式幫我們解決問題的時候,總是得不到想要的結果,尤其是在使用函式式的工具庫進行函式組合的時候,斷點除錯都很困難,就算你把斷點打到了第三方的工具庫裡面,你真的確定你能在短時間內看懂人家的原始碼嗎?這個時候往往只能想辦法去console.log。不過這個問題是可以通過不斷的學習總結來解決的。即便使用其他的方式程式設計,不熟悉所用的工具也會出現同樣的情況,這並不是函數語言程式設計的問題。

絕大多數的bug都是由副作用引起的。也就是系統狀態的變化。這一點無法通過學習來解決,你能記住成千上萬個狀態,並能時刻了解他們的變化嗎?可如果我們是在函數語言程式設計,使用了大量的純函式,由於純函式不產生副作用,在除錯的時候,我們只需要去除錯會產生副作用的部分就可以了。

在函數語言程式設計中,一切的技巧的目的都是為了讓你的程式碼更加的函式式。可掌握有些技巧的成本很高,這就導致部分人對函數語言程式設計的”可讀性“產生了質疑。

其實,函式式程式碼的可讀性與讀程式碼的人的函數語言程式設計的能力有直接的關係。如下圖:

JavaScript函數語言程式設計之為什麼要函數語言程式設計(非嚴謹技術層面的扯淡)

指令式程式設計很符合人類的思維習慣,入門的成本很低,而宣告式的函數語言程式設計的學習則有一定的門檻,對於新手來說,可能幾乎沒有可讀性。只有不斷的深入學習,這種可讀性的優勢才會逐漸的體現出來。這可能是導致函數語言程式設計沒有被廣泛應用的最主要原因。

函數語言程式設計的一些問題

  1. 可讀性的疑惑。pointfree的宣告式的寫法有時候很難講到底是提高了可讀性還是降低了可讀性,特別是在你沒有在函式的命名上很好的體現出這個函式的功能、引數和返回值的詳細資訊的情況下(能提現出來嗎?)。真正的函數語言程式設計語言有型別簽名,當然你可以用這種方式給你的函式加上註釋。可是,熟練掌握型別簽名也不是一件容易的事情。
  2. 用的人太少。可能是入門門檻的問題,現在大多數的程式設計還是物件導向的。如果僅僅是用一個函式式的工具庫幫助處理一下資料可能不是大問題。可如果你把各種進階的技巧如函子、範疇學的公式等在實際專案中用的不亦樂乎,你的不太熟悉函數語言程式設計的同事可能會罵街。
  3. 函數語言程式設計通常會大量的使用柯里化,理論上會影響效能,進而影響體驗。可由於執行環境種類太多,這一點不好量化,要根據具體的情況討論。
  4. 習慣了指令式程式設計,轉變成函數語言程式設計會有點困難。你的物件導向用的越好,可能轉起來就越困難。我(我物件導向用的不好)目前在用函數語言程式設計時,很多時候都是先用命令式的方式想清楚,再改成函式式。在腦袋裡面裝一個編譯器,十分費電。

如果還要舉的話。。。

  1. 程式碼量很大的時候,宣告式的程式設計,給函式起名字會很辛苦。。。。。。。吧?

函數語言程式設計與物件導向程式設計

物件導向程式設計更傾向於封裝,而函數語言程式設計更傾向於抽象。

物件導向程式設計抽象的結果是一個類,函數語言程式設計抽象的結果是一個過程(一個函式)。

封裝和抽象有什麼區別?

他們並沒有明顯的界限,抽象是通過封裝來實現的,我找不到一個只是抽象而不是封裝的例子。封裝和抽象的目的略有不同。簡單的講,封裝只是把一些會再次使用的操作包起來,等待下次使用。抽象也會有“包起來”的動作,可它更多的目的在於要劃清界限。這就好比讓我們解釋一個概念,如果你說“我懂,但我就是說不出來”,那麼大概率你是沒有懂。我們是否能清晰準確的說清楚,取決於我們腦中對這一概念的界限是否劃清楚了。如果沒有劃清楚,我們就沒辦法下斷語去說明它是什麼和它不是什麼。抽象也是如此,它需要我們搞清楚它是幹什麼的,把不應該乾的事情都去掉。“它是什麼”這一點越明確,那麼這一封裝的抽象程度就越高。

參考資料:

相關文章