函數語言程式設計(一)
什麼是函數語言程式設計
函數語言程式設計是一種程式設計正規化,常見的程式設計正規化有以下三種:
- 指令式程式設計
- 宣告式程式設計
- 函數語言程式設計
函數語言程式設計的本質是將計算描述為一種表示式求值。在函數語言程式設計中,函式作為一等公民,可以在任何地方定義(在函式內或函式外),可以作為函式的引數和返回值,可以對函式進行組合。
函數語言程式設計的準則:不依賴於外部的資料,而且也不改變外部資料的值,而是返回一個新的值給你。看個簡單的例子:
// 非函式式的例子 let count = 0; function increment() { count++; // 依賴於函式外部的值,並改變了它的值 } // 函式式的例子 function increment(count) { return count++; }
為什麼採用函數語言程式設計
函數語言程式設計不依賴外部的狀態也不修改外部的狀態,函式呼叫的結果不依賴呼叫的時間和位置,這些寫程式碼容易進行推理,不易犯錯,而且單測和除錯都更簡單。即函式程式設計採用純函式。
純函式是這樣一種函式,即相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用。
副作用可能包含,但不限於:
- 更改檔案系統
- 往資料庫插入記錄
- 傳送一個 http 請求
- 可變資料
- 列印/log
- 獲取使用者輸入
- DOM 查詢
- 訪問系統狀態
副作用是在計算結果的過程中,系統狀態的一種變化,或者與外部世界進行的可觀察的互動。
純函式的好處:
純函式能根據輸入來做快取(memoize技術)
const memoize = function(f) { const cache = {}; return function() { const argStr = JSON.stringify(arguments); if (!cache[argStr]) { cache[argStr] = f.apply(f, arguments); } return cache[argStr]; } }
可移植性/自文件化
純函式的輸出只依賴與它的輸入,依賴很明確,易於理解。由於純函式不依賴它的上下文環境,因此我們可以輕易的把它移植到任何地方執行它。
可測試性
我們不必在每次測試前都去配置和構造初始環境,只需簡單給函式一個輸入,然後斷言它的輸出就好了。
合理性
由於純函式總是能夠根據相同的輸入返回相同的輸出,所以它們就能夠保證總是返回同一個結果,這也就保證了引用透明性。
並行執行
我們可以並行執行任意純函式。因為純函式根本不需要訪問共享的記憶體,而且根據其定義,純函式也不會因副作用而進入競爭態。
並行程式碼在服務端 js 環境以及使用了 web worker 的瀏覽器那裡是非常容易實現的,因為它們使用了執行緒(thread)。不過出於對非純函式複雜度的考慮,當前主流觀點還是避免使用這種並行。
實現函數語言程式設計的技術
這裡我們先不展開這些技術的細節內容,本文我們先側重於對函數語言程式設計有一個整體上的認識,具體的技術細節我們將在下一章展開。
- curry(柯里化)
- compose(程式碼組合)
- Monad(Monad就是一種設計模式,表示將一個運算過程,通過函式拆解成互相連線的多個步驟。你只要提供下一步運算所需的函式,整個運算就會自動進行下去。)
如何正確看待函數語言程式設計
我們先來看以下幾種觀點:
- 你這段程式碼用了 for 迴圈,這是過程式的。為了優雅,你應該寫成函式式的。
- 你這段程式碼有副作用,這是骯髒的。為了純淨性,你應該把 IO 包在 Monad 裡。
- 你這段程式碼用了 class,這是面向物件的。為了無狀態,你應該寫成高階函式。
我想說的是這種偏激的觀點是不正確的,我們不應該把函數語言程式設計和指令式程式設計對立起來,我們更多的時候需要考慮的是技術的適用場景。函數語言程式設計寫起程式碼來,有一定的難度,如果一個團隊的整體水平達不到,那麼寫程式碼的質量和效率還不如採用指令式程式設計好。函數語言程式設計利用純函式的無狀態性,它的好處非常多(結果可預期、利於測試、利於複用、利於併發),但一個系統工程的程式碼,是不可能全部採用純函式來寫的。當我們越貼近業務,我們就離純函式與無狀態越遠。
函數語言程式設計非常重要,學習它我們能開啟我們的思維方式,使用它也有很多好處,但它也有一些侷限,我們應該客觀看待。保持開放的心態,根據實際場景選擇合適的技術,是一個工程師基本的素養。