關於本文:
本文發表於前端早讀課【第888期】
往期回顧:
下一步
你已經學了函數式編程相關的所有新知識,你可能開始困惑:“然後呢?我如何才能在我的工作中實踐起來?”
這得具體問題具體分析。如果你工作中用的是像Elm或者Haskell這類純函數語言,那這些知識很容易就能運用起來。
如果你只能用某種命令式語言(這種情況很常見),比如JavaScript,你還是能運用大部分我們之前提到的知識點,不過會有很多的限制。
函數式JavaScript
JavaScript擁有很多類函數式的特性。JavaScript沒有純性,但是我們可以設法得到一些不變量和純函數,甚至可以藉助一些庫。
但這並不是理想的解決方法。如果你不得不使用純特性,爲何不直接考慮函數式語言?
不變性(Immutability)
首先要考慮的點就是不變性。ES6新增了一個關鍵詞const
,它意味着一旦被賦值,就不能重新設置:
const a = 1;
a = 2; // 拋出類型錯誤的異常
此處a
定義爲常量,也就是說一旦被設定就不能再修改了,這就是爲什麼a = 2
會拋出異常錯誤(除了Safari外)。
const
的缺陷在於它不夠嚴格,我們來看個例子:
const a = {
x: 1,
y: 2
};
a.x = 2; // 沒有異常!
a = {}; // 拋出異常:類型錯誤
注意到a.x = 2
並沒有拋出異常。用const
關鍵詞限制的只有變量a
本身,a
所指向的對象是可變的。
這很槽糕。本來以爲有了const
,JavaScript會更加完善。
那麼問題來了,如何才能在JavaScript中得到不變性。
不幸的是,只有Immutable.js庫能做到。雖然它能保證更好的不可變形,但悲劇的是,它以更像Java的方式寫我們的JavaScript。
柯里化與整合(curring and composition)
之前我們已經學會如何將函數柯里化,舉一個複雜的例子再回顧一下:
const f = a => b => c => d => a + b + c + d
我們得手寫上述柯里化的過程。然後用如下的方式調用:
console.log(f(1)(2)(3)(4)); // 打印出 10
括號之多,連寫Lisp的程序員都要hold不住了。
簡化上述過程的庫很多,我最喜歡用的庫是Ramda。
我們用Ramda去改寫上面的例子:
const f = R.curry((a, b, c, d) => a + b + c + d);
console.log(f(1, 2, 3, 4)); // 打印出 10
console.log(f(1, 2)(3, 4)); // 打印出 10
console.log(f(1)(2)(3, 4)); // 打印出 10
函數定義的方法並沒有改進多少,但在調用的時候可以避免寫那麼多括號了。調用f
的時候,你想任意指定參數的個數。
我們重寫一下之前的mult5AfterAdd10
函數:
const add = R.curry((x, y) => x + y);
const mult5 = value => value * 5;
const mult5AfterAdd10 = R.compose(mult5, add(10));
事實上Ramda提供了很多輔助函數來做些簡單常見的運算,比如R.add
以及R.multiply
。以上代碼我們還可以簡化:
const mult5AfterAdd10 = R.compose(R.multiply(5), R.add(10));
Map,Filter和Reduce
Ramda也有對應的mao
,Filter
以及reduce
函數。在原生JavaScript中,這幾個函數是在Array.prototype
對象中的,而在Ramda中它們是柯里化的:
const isOdd = R.flip(R.modulo)(2);
const onlyOdd = R.filter(isOdd);
const isEven = R.complement(isOdd);
const onlyEven = R.filter(isEven);
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(onlyEven(numbers)); // 打印出 [2, 4, 6, 8]
console.log(onlyOdd(numbers)); // 打印出 [1, 3, 5, 7]
R.modulo
接受2個參數,被除數和除數。
isOdd
函數表示一個數除2的餘數。若餘數爲0,則返回false,即不是奇數;若餘數爲1,則返回true,是奇數。用R.filp
置換一下R.modulo
函數兩個參數順序,使得2作爲除數。
isEven
函數是isOdd
函數的補集。
onlyOdd
函數是由isOdd
函數進行斷言的過濾函數。當它傳入最後一個參數,一個數組,它就會被執行。
同理,onlyEven
函數是由isEven
函數進行斷言的過濾函數。
當我們給函數onlyEven
和onlyOdd
傳入numbers
,isEven
和isOdd
獲得了最後的參數,然後執行最終返回我們期望的數字。
JavaScript缺陷
借用了庫和語言增強工具,JavaScript已經能做那麼多事情了,但它仍然有個致命的缺陷——它是命令式語言,卻想什麼都做。
絕大多數的前端開發不得不使用JavaScript,因爲它是瀏覽器唯一接受的語言。不過,也有很多開發者繞過了這個坑——他們用另一種語言編寫,然後編譯成JavaScript。
CoffeeScript是這類語言中最早的一批。目前,TypeScript已經被Angular2採用。Babel可以將這類語言編譯成JavaScript。
越來越多的開發者在項目中採用這種方式。
但這些語言本質上還是JavaScript,並未有明顯改善。爲何我們大膽嘗試直接使用一門純函數語言,然後轉譯成JavaScript?
Elm
這系列文章,我們已經借用Elm來幫助我們理解函數式編程。
那Elm是什麼?怎麼用?
Elm是一種能編譯成JavaScript的純函數語言,你可以用Elm腳手架搭建一個Web應用。Elm腳手架,全稱爲The Elm Architecture,簡稱TEA,它是Redux的啓蒙者。
Elm程序在運行時不會報錯。
Elm在一些公司中已經投入了應用,比如NoRedLink。Evan Czapliki大神,Elm的作者,目前在這家公司工作(準確地說,他在Prezi工作)。
更多信息參見6 Months of Elm in Production,NoRedInk的Richard Feldman關於Elm的分享。
我要用Elm取代所有JavaScript嗎?
不用,你可以逐步地取代。具體可參考教程How to use Elm at Work。
爲什麼要學Elm?
- 純函數式編程具有約束和釋放雙重特性,它約束你的行爲(通常是避免了給自己挖坑)同時讓你遠離bug和槽糕的設計。因爲所有的Elm程序都要遵循Elm腳手架的規範。
- 學習函數式編程能讓你成爲更好的程序員。本文涉及到的只是函數式編程的冰山一角。你應該去實踐一下,感受它的魅力,感受它是如何減少你的代碼量以及增加穩定性。
- JavaScript最初是在10天內倉促地完成的,然後在過去的20多年裏一直在打補丁,直到現在變成了一門有一點點函數式,一些些面向對象,完完全全的命令式編程語言。Elm學習了Haskell社區過去30多年的精華,而Haskell社區也有數十年的數學和計算機工作的沉澱。並且Elm腳手架的設計是Evan在函數響應式編程方面發表的論文結果實現,這幾年來一直不斷改善優化。(Controlling Time and Space 提到了它的設計理念)
- Elm是爲前端開發人員而生,旨在簡化開發工作。(Let’s Be Mainstream深刻地說明了這一點)
未來展望
我們無法預知未來的趨勢,但至少我們可以做有根據的猜測。以下是我的一些拙見:
能轉譯成JavaScript的這類語言將會有大進展;
存在40多年的函數式編程思想將重新被挖掘出來,用來解決我們目前遇到的複雜問題;
目前的硬件,比如廉價的內存,快速的處理器,使得函數式技術普及成爲可能;
CPU不會變快,但是內核的數量會持續增加;
在複雜項目系統中可變性將成爲最大要害之一。
我之所以寫這系列文章,是因爲我相信函數式編程是未來趨勢,而且過去幾年我學得挺費勁,當然我現在也還在學習中。
我希望我能幫助你們更容易更快地學會這些概念,幫助你們提升技能,然後未來有更好的出路。
即使我預言Elm將會普及的觀點是錯誤的,我敢說函數式編程和Elm語言是未來中的一部分。
我希望你讀完這個系列之後,你對這些概念有了清晰的掌握,對自己也更有信心。