近幾年來,JavaScript帶起不少關於函數式的探討,然而,多年過去了,到現在仍有開發者在問:函數式帶來的啟發價值何在?真的具有實用性嗎?

一級函式不一定必要

在命令式語言中,JavaScript經常被用來示範函數式設計,有開發者會說,這是因為,函式在這個語言中是一級物件。不過,若只是要實現函數式的典範,一級函式並不是必要的條件。

來看看Java吧!Java 8引入了Lambda專案,而Lambda API也有不少函數式的概念,雖然Lambda語法、方法參考(Method reference)看來像是一級函式,很大程度上卻是語法蜜糖。在Java 8之前,若能對可讀性有所幫助,或者是取得惰性處理上的益處,此時,我們透過程式庫的輔助(例如Guava的FluentIterable等),也可以進行函數式的設計。

若程式語言支援一級函式,此時,我們就可以直接將函式傳遞給函式,或者從函式傳回函式。若有個函式可以接受函式且傳回函式,這樣的函式會有個酷名字——高階函式(High-order function),開發者甚至可以藉此實作出函式合成(Function composition)這類神秘的功能;不過,就算使用物件,也可以模擬出來,就是在實作時會花費比較多程式碼罷了。

一級函式真正的意義有兩個:一是可以傳遞程式碼,二是只以函式的粒度來思考。如果使用物件,當然也可以傳遞程式碼,若物件上會有多個方法(也就是多個函式),所傳遞的實際上就會是一組函式,而不是一個函式,同時,這往往導致從物件取得過多資訊,來做過多的事情;若限制為傳遞函式,那就只需用該函式來思考,令事情單純化,如果試圖傳遞多個函式,我們很容易就會發現函式簽署變複雜了。

並非全部不可變

令事情單純化,是傳遞函式的目的之一,然而,以JavaScript來說,沒有人能阻止開發者傳遞幾百或幾千行的函式內容,若是如此,就算可以傳遞函式也沒有意義。

目前有什麼方式,可以強制開發者寫出意圖單一、實作單純的簡明函式?這時,我們定會談到不可變(Immutable)特性了,就JavaScript而言,可以使用const來宣告變數,使用Object.freeze凍結物件,令其成為唯讀物件。

當開發者無法變動變數值或物件狀態,就只能以基於既有變數值或物件狀態,來產生新值或物件狀態,這不就是個數學函式的概念嗎?因此,大任務會被逼著切割為小任務,然後,就會很容易察覺小任務的流程重複性──JavaScript開發者最常舉的例子,就是filter、map這類的函式,因為它們封裝了重複的小任務,就可以在呼叫它們時,只專注在真正該關心的演算,並以函式傳遞演算。

若函式實作時採用了不可變特性,該函式就會被稱為純函式(Pure function),相對地,就會是具有副作用(Side effect)的函式,不過,這麼一來,很容易會有開發者問到:「真實世界中,真的有專案全部都用不可變來寫嗎?」答案是沒有!因為對電腦的運行來說,整個程式都是純粹的這件事,本身就沒有意義!

就算是在Haskell等純函數式語言中,也是有副作用的部份,不然就無法接受輸入,也不能進行輸出,其實純函數式要求的是,副作用函式與純函式有個明確的界線,一邊是完全純粹的世界,不純的世界是另一邊;副作用本身對電腦上運行的程式是必要的,它並非萬惡,真正怕的是:開發者搞不清楚這函式是純或是不純,進一步導致他們在不知道的地方改變了什麼。

對於非純函數式語言來說,能不能用上純函數式的概念,以及是否實用,主要是看這門語言,可以在純與不純的界線上支援到什麼程度。

例如,純函式在重複演算時會用上遞迴,如果語言可以支援遞迴最佳化(像是尾遞迴),那麼,實用性就會提升,然而,就算是純函數式語言,也是隱藏了電腦不純的事實。

對於非純函數式語言來說,如果語言本身在純與不純的界線上,支援度不夠,還有另一個可能性,那就是需透過程式庫或框架來實現這條界線,令開發者這程式庫或框架的這條界線之上,去實現純粹的世界,至於不純的部份,該怎麼做、該怎麼最佳化,就用程式庫或框架來隱藏,這麼一來,開發者就越能以純粹、宣告式的方式來思考,避免不純粹、命令式的複雜性。

前端與函數式

以純粹、宣告式的方式來思考,指的是用更高階的方式來思考。例如,在非函數式語言中,使用到for迴圈,往往涉及低層次的變數指定、遞增等細節,在這之前,若能想想迴圈走訪真正的目的是什麼,是要對元件進行過濾、轉換或其他的?這就是屬於高階思考!

此時,有的開發者就會說了:「所以呢?這邊最後還是在探討filter、map而已嗎?JavaScript對函數式的探討怎麼最後都是在談filter、map呢?」實際上,近年來前端有不少實務性框架,都具備函數式的特徵了,例如,Reac本來就鼓勵開發人員寫純函式元件,React Hook更進一步地加強這特性,就算是在函式中,也希望開發者區分純與不純。

而且,「高階」這兩個字其實有意思,經常出現在函數式的設計。例如,在React中,有所謂的高階元件(High-order component),表示該元件可以接受元件,傳回新的元件,如果知道函數式中高階函式是怎麼一回事,那麼,高階元件不過就是把「函式」一詞換為「元件」罷了。

既然React鼓勵寫純函式元件,至於Redux的話,希望開發者寫Reducer時是純的,在程式庫或框架的輔助下,開發者一開始就用純粹的角度來思考,確實能對整個程式有比較好的控制,之後再依需求,將必要的部份以不純的方式來實現,並與純函式隔離,如此開發者才能清楚認識與掌握不純的部份。

函數式約束之目的?

正如不該為了物件導向而物件導向,函數式也不是萬靈丹,我們應該將純粹看成是種約束,避免一開始就不純,讓程式隨意暴走,這種做法特別針對的目標,主要是那些自我約束程度低、能力不足、又不太思考就來寫程式的開發者而言。

以Haskell而言,是由編譯器來做這個強制,JavaScript可以用Object或Reflect API,來實現不可變物件;另外,就是靠慣例或者強制用const來宣告變數之類,或者使用React、Redux之類的框架對開發者的約束,來實現這種強制性。

因為有了限制,不熟悉函數式的開發者,才會覺得React、Redux比較難以使用,然而,對於純函數式若有足夠的認識,在使用這類程式庫或框架時,能更清楚它們為何是如此設計及要求,也更能在不抵觸其約束下,設計出純粹的函式或元件。

重要的是,規畫者要清楚,函數式的約束是為了什麼,不然,反徒增團隊困擾。這就跟使用框架一樣,如果採用者不清楚,採用框架的約束力是為了得到什麼好處,只是為了用框架而用框架,那就只能住套房了。

函數式的約束也是同樣的道理,如果我們不清楚函數式約束是要換來什麼好處,硬是要求不可變特性等,最後就只會遞迴遞到死了。

作者簡介


Advertisement

更多 iThome相關內容