曾幾何時,看到某個開發者談及函數式程式設計時,已經算不上是新鮮之事,不同語言生態圈的開發者,會用各自熟悉的語言、程式庫或框架來實現並闡述函數式設計的概念,只不過,在試圖從這類語言、程式庫或框架中探討函數式概念時,總有種朦朦朧朧、看不清楚真貌的感覺。既然如此,何不來實際看看一門真正的函數式語言呢?

探索函數式元素的源頭

這年頭做為一個開發者,或多或少都有聽過函數式程式設計這個名詞,不少主流語言中,也已經或逐步出現函數式程式設計的基礎元素,有的具備一級函式、高階函式等概念,像是JavaScript、Python、Ruby,有些直接將物件導向與函數式並列為主要典範,像是Scala,就連Java這個保守的語言,在Java 8中,除了Lambda語法本身具有一級函式概念之外,也突然出現了不少函數式概念的API,更高階的典範,如Functional Reactive Programming,即是以函數式作為基石。

對這類不是以函數式為主要典範的主流語言而言,為了讓函數式元素在其本身中不至於過於突兀,這類元素多多少少都有經過一些調整,相關的調整是必要的,這也是函數式程式設計得以逐漸為開發者接受的主因之一,經過調整後,才使得讓這類元素得以成為開發者使用的選項之一。

只是語言的特性各有不同,調整的方式也就各異,因此,同一個函數式概念,在不同語言中可能會有截然不同的實現或詮釋。

就因為自己常有這類霧裡看花之感,幾年前開始認真地學習一門純函數式語言Haskell,也察覺到多種不同的函數式實現,其實可能都是同一個函數式概念。

期間,我亦曾認真將幾十個常見資料結構演算,用Haskell實作,以熟練並逐步發掘出進行函數式設計時的一套思路,從而影響了在使用主流語言時的思考方式。例如,在我的一些文件、研討會或專欄中,為了能用最純粹的方式解釋某個函數式概念,偶而也會看到直接使用Haskell作為示範。

使用Haskell來表達某個函數式概念很純粹,只是每當打算這麼做之前,總會自問一次,這麼做是否有賣弄文采、故弄玄虛之嫌,當看到Java 7決定重新將Lambda特性納入(後來是在Java 8實現),套個常用的標題殺人法:「我簡直驚呆了」,只能說覺得自己撿了個大便宜。

然而,這也表示了,函數式程式設計已經歷經時代的考驗,開發者應該認真地考慮,從一門純函數式語言中,像是Haskell,去瞭解真正的函數式設計。

從Hello, World概覽Haskell全貌

要學習Haskell,必須先有個編譯器,如果使用GHC(Glasgow Haskell Compiler),安裝好之後,實際上也會有個GHCI直譯環境可以使用,多數的文件或書籍會從這個環境開始介紹Haskell,因為這樣可以不用一開始接觸那麼多觀念,不過,對於一個熟練的開發者,若也能寫個可接受使用者輸入並顯示的「Hello, World」原始碼,並能編譯與執行,就可以進一步概覽Haskell全貌。

透過GHCI介紹Haskell的文件中,多半會提及Haskell的型態系統。一開始,最令人印象深刻的是Haskell屬於靜態定型語言,然而多數情況下不用寫出型態,編譯器會為你推斷出最適合(通常是最寬鬆)的型態,但是,這並不代表著它是弱型別語言,例如,Haskell中不能1+"2"不稀奇,如果x=10,y=3.14,x+y無法通過編譯,或者某函式的參數為Float,指定1作為引數呼叫卻無法通過編譯,這就令許多開發者詫異了。

有時在Haskell中,使用函數式風格並不是最難的,使用正確型態通過編譯才是最難的,因為嚴格的強型別、靜態型態,使得在Haskell中要通過編譯本身就是件難事,因此有「It Compiles! Let's ship it!」的笑話!

然而換取而來的代價是,不少因型態不正確的錯誤,在通過編譯之前都被抓出來了,很多時候確實是如此,我以為自己早謹慎思考過型態了,編譯器卻總會抓出我沒想到的部分。

在第一個「Hello, World」程式碼中,可能會接觸到一些函式,稍微遇到惰性,然後,會開始遇到Haskell中沒有變數這個事實。基本上,只能令某個名稱為某值,之後,在必要的時候,引用該名稱來取得值。

在非函數式語言之中,這個Immutable特性必須特意為之,在Haskell卻是尋常之事,因為這樣做,函式就不會有副作用,也就因此函式一定要有傳回值,所以,第一個「Hello, World」,不可避免地一定會接觸到Monad,這個現今時不時會出現在某開發者部落格或研討會的奇妙概念。不過,嚴格來說,出現次數之多,也讓Monad漸漸剝離了一些神祕色彩。

這裏,那裏,到處都是函式

或許該拜JavaScript風行之賜,函式可作為值傳遞的一級函式概念,對現今開發者來說,多半已不陌生,在Haskell中幾乎所有運算都與函式相關,就連`+`、`-`、`*`、`/`也都是函式,願意的話,可以定義add=(+),就可以使用add 1 2或1 `add` 2來進行相加運算。

函式有型態,(+)是個接受兩個引數的函式,型態為Num a => a -> a -> a,這表示a必須是個有Num行為的值,a -> a -> a則揭露了在Haskell中,多參數函式其實是由多個單參數函式組成的事實。

多個單參數函式組成多參數函式?這可以將add 1 2這樣的函式呼叫,改寫為(add 1) 2來理解,(add 1)執行後實際上傳回一個函式,因此可以用2為引數,繼續來呼叫這傳回的函式,在型態上,也就是可將a -> a -> a寫為a -> (a -> a),其中(a -> a)表示傳回了一個單參數的函式。

不少開發者曾在JavaScript中看過,有人試著用奇妙的方式實現出這種效果,並給了它一個名稱叫做Curried function,因為實現方式太奇怪,有些開發者可能不明白為什麼要做這種設計。然而,在Haskell中,這是很自然的一個操作,因為(+)是個雙參數函式,如果需要一個將某數加3的函式,只要定義addThree=(+3)就可以了,因為(+3)傳回了一個新函式,那麼addThree 4就會是7。

就因為函式應用在Haskell中到處都是,從任何既有的函式產生新函式也就是再自然不過的手法。隨之而來地,可接受函式作為引數或者可傳回函式的所謂高階函式,也就不會是什麼神秘的東西。

就拿經常被討論的高階函式map、filter為例好了,想把一組[1, 3, 2, 4, 5]的清單全部加3,那就是map (+3) [1, 3, 2, 4, 5]。而想要過濾出大於3的數字,那就是filter (>3) [1, 3, 2, 4, 5],這是因為(>)也是個函式。

從話題變為常識

在《松本行弘的程式世界》的〈實用主義〉中談到:「新出現的熱門主題經常引起爭論,誰都知道的常識則幾乎不會引發爭論。」文中指的是物件導向,這個過去即使只是談封裝、繼承、多型就可能引發爭論的典範,現在卻成了開發者必備的常識,想用物件導向來引發一些話題,大概就只剩「物件導向已死」這種標題,勉勉強強可以達到了。

類似地,在主流語言中談到Immutable、一級函式、高階函式,甚至是Monad、Curried function等源於函數式的概念,現在還算是新鮮話題,然而,別忘了,這些是經過某些調整,以適切於某些主流語言。

如果在主流語言中,對這些概念的感覺過於矇矓或複雜,也許是直接看個純函數式語言的時候了,就現今而言,研究一門純函數式語言,像是Haskell,已經不算是曲高和寡,反倒是採取實用主義的一種表現了。

作者簡介


Advertisement

更多 iThome相關內容