半年前認識了 Haskell 後簡直愛上了它,但是過了半年還是沒做到到什麼具體的東西出來。上星期想到有個小東西想做,於是就挑戰一下純用 functional language 來做好整個網。Backend Haskell, frontend elm。
Source: https://github.com/b123400/whosetweet
這篇文不是教學文,我也盡量避免技術細節,只是希望分享一下我在學習途中遇到的困難和想法,希望可以吸引一下大家來學 functional programming 或者是學之前有些心理準備吧。
我一開始打算學 FP 的時候其實是因爲 erlang 的名字很帥,但是 Haskell 的 logo 比較好看所以還是學 Haskell 吧。但看了幾篇教學文之後就發覺這東西真的很難學習。
首先當然是 hello world:
main = putStrLn "hello, world"
好像也沒什麼問題。但是下一步就開始說明,其實這個不是print東西,而是return一個IO ()
的東西給main
,至於IO ()
是什麼呢?就要先理解Monad,但是Monad和IO的關係是什麼呢?就要理解 Haskell 的 type system。於是就要看關於 type 和 typeclass 的文章… 每一個概念都基於另一個概念,要學習的話必須衝得夠深,把它記住,再理解,才能學懂另一個概念,不夠專心的話真的很難。花了兩個星期,有一天我在夢中醒來,突然明白了Monad究竟是什麼,真的好神奇。然後我明白IO是什麼的時候才發覺,我好像還沒明白後面那個()
是什麼意思…
另外,這個 community 裡的人也很喜歡用 infix function,也就是說看起來很像 operator 的 function,比如說 Text.XML.Cursor 有一堆(&|),(&/),($.//),($./) 這類的function,連發音都不知道,第一次看見的人根本不知道在幹什麼的,要 search 也有點困難,畢竟 google 是找不到這種符號的。於是就需要乖乖地去看 doc,好好理解之後,就開始覺得這樣寫其實滿爽的。順帶一提想 search 的話去 Hoogle 吧。
學其他語言的時候我多數都是先理解語言裏的基本概念,然後覺得夠熟的話就變成找我需要的 api ,然後就直接寫 code 了,但 Haskell 裏面有超多抽象的概念,就算覺得自己已經學夠了,隨便找一個 library 都會發現又有新東西要學。想用 Yesod 就發現它有自己的 template langauge,想用 twitter-conduit 就要明白 lens。和那些從 w3schools 上隨便 copy sample code 就可以直接跑了的語言真的差很遠,不認真理解的話幾乎無法使用,而有趣地大部分教學文都是一大段文字說明概念,再附上幾行 code 這樣,沒耐性的話真的很難學。
想法和普通的語言差很遠,沒有變數,也就沒有loop,也不可以有 side effect,很多時候都要看其他人的 code 才知道應該如何做。熟悉其他普通的語言的話就會覺得要用這麼彆扭的方法來完成一件簡單的事情實在是很奇怪,但是認識多一種 modelling 問題的方法也是很有趣的。要學懂放開平時一直在用的那套思維,用另一種思考方式來面對問題,這點其實在 programming 以外的地方也通用吧。
有限制不等於功能少了,我最喜歡的例子應該是 currying 吧。在 Haskell 裡每一個 function 只可以收一個 argument然後 return 一個 value,不多不少。那麼如果想有一個 function 是收幾個 argument 的話怎麼辦?答案是:return 一個收多一個 argument 的 function。如果用 JS 寫的話大概是這樣吧:
add = (a)=> (b)=> a + b
add(1)(2) // 3
這個不用打破限制,卻能準確地解決問題的想法,不覺得很漂亮嗎?Monad 由於太難介紹了所以只好割愛,但也一樣是一個很簡單很漂亮的問題解決方法。
看到這裏也許你會想,所以 Monad 到底是什麼?我也想簡單地說明,但實在太難了。這個與其說是一種功能,與其說是一種設計,不如說是一個 pattern,一種承諾。就好像問到「數字是什麼」,我們也許會說就是 1, 2, 3… 但是想深一點,f
在16進制也是數字啊,那麼「一」算數字嗎?那麼虛數是數字嗎?想到這個地步,根本沒有人能淺白易明而準確地介紹數字這個概念了。
而這個也正是很多人卡住了的地方, Monad 實在太抽象了,幾乎什麼都可以是一個 Monad,但是這樣就等於什麼都沒說明過,general 到了這個地步,其實就好像說了很多,不過什麼都沒說過一樣。那麼 general 到了這個地步有什麼好處呢?就是一個超通用的問題的解決方法啊!就好像我們會嘗試用抽象的概念(比如 Promise,其實 Promise 就是一種 Monad)來整理 code 一樣,Monad 也只是一種更加抽象的概念而已。至於我說 Monad 很漂亮其實就就如 JS developer 會說 promise chain 起來很好看一樣而已。透過多用這些抽象的概念,code 的 reusability 也會提升。
(有興趣的了解的話我十分推薦這篇文:Functors, Applicatives, And Monads In Pictures)
Static 的確比較安全,但很多時候只針對「你有沒有搞錯了type」的問題,而不是「你有沒有好好處理」這點。我在寫 swift 時經常都會這樣的:
if let sth = something {
doesntAcceptOptional(sth)
}
的確有好好做 checking 所以沒問題,但是卻沒有想過爲什麼 something 會有可能是 nil 呢?nil的話怎麼辦呢?至少我都懶得想。但在 Haskell 裏面每個 function 都一定要 return 一些東西,
case something of
Just sth -> doesntAcceptOptional sth
Nothing -> -- 這裏寫什麼好?
不處理的話就無法 compile,如果 return Error type (Maybe / Either) 的話那麼我就是把處理 error 的責任交給了 call 這個 function 的 code 了。沒有逃避 Error handling 的方法。也就是說大部分情況只要能 compile 的話基本上都沒什麼問題的,超強的安心感。
此外,由於 function 的內容受制於 type,而又不可以有 side effect,很多時候發現自己當初想漏了點 edge case 的話就要從新 model 過問題,然後就要重新把 function 寫過,然後就發現自己在 refactor了,最常見的話應該是一些有機會失敗的地方應該要用 Maybe 吧。我一開始基本都是隨便寫幾行以爲沒問題,之後發現這樣下去結構會很難繼續寫,於是只好重新想過整個 data flow,然後又發現好像有點不對… 這種設計令我最終寫出來的 code 一定是超精準的,當然也很花時間。如果你喜歡隨便找 workaround 的話就應該不太適合了。
這個不得不說,Haskell 的 error message 真的很難看,與其說是 Haskell 的問題不如說是本質吧。比如說那堆 (&)(&|)(<*>)(<$>) 的 function 不小心打錯了就變成了另一個 function,然後就會出現 type 不對的 message。還有打漏了 argument 的話會被以爲是故意的(因爲可以curry),然後 error 又會變成 type 不對,打漏了個逗號的話也會被以爲是 function call,所以又是 type error。基本上我都沒遇過什麼 syntax error,不關事 type error 倒是一大堆。
說了這麼多,好像缺點多於有點啊,那爲什麼要學呢? 當然是因爲漂亮啊!Currying 是多麼漂亮的想法,明明有着「只有可以拿一個argument」的限制,但用 curry 起來的話那多少個都沒問題了,既沒有破壞限制,也很優雅對不對?還有 Monad 的想法也是很簡單地把常見的 pattern 用安全的方法 implement 出來。
當然啦,這一切其實都不是必須的,而且看看 PHP 和 JS 就知道不漂亮根本就不是問題。我也覺得如此難學習的東西,如此多限制的東西要流行起來很困難,尤其是很多時候我們願意把對「漂亮」的要求降低一點來換取開發速度、容易招聘人才等等。如果說用一般的 programming language 來開發像是寫文章的話,那麼 functional programming,特別是 Haskell 應該就像是寫一首押韻的七絕吧。
如果你是 JS developer 的話,從 LiveScript 開始的話應該滿容易的,它不是 functional ,但你可以寫得很 functional,也有 currying, do 和 back arrow 的 syntax 和 Haskell 裡的概念也有幾分相似,一邊看着 compile 出來的 JS 一邊寫應該不會很難吧。
如果想簡單地做點什麼來試試看的話,elm 也很不錯。elm 是一個參考了 haskell 的 compile-to-JS language,連 compiler 也是用 haskell 寫的,也是 static, functional。特點是作者爲了方便入門做了很多削減,比如說沒有 typeclass,而 Monad 亦被削成了 Task,這樣當然失去了 Monad 的萬能性,不過想簡簡單單地體驗的話還算不錯吧。你可以叫 elm 只在指定的 element 上 run,所以如果不放心的話可以找一個小小的部分先開始試一下。和 elm 和 JS 之間的溝通也是很簡單的,沒什麼陷阱。
以上兩個 JS 的 language 雖然不差,但是如果想認真玩 functional programming 的話我還是覺得用 Haskell 比較好,畢竟 JS 本身不是爲了 functional 而設計,用 elm 一個不小心很容易就會 recursion 到爆 stack。