Haskell Tutorial(3)初探代數與函式 << 前情
到目前為止,基本上你已經知道如何定義一個函式,大致上也瞭解如何宣告函式的型態,例如,在〈Haskell Tutorial(2)一絲不苟的型態系統〉中,定義過一個 doubleMe 函式:
doubleMe :: Float -> Float
doubleMe x = x + x
根據 doubleMe :: Float -> Float ,你知道函式名稱是 doubleMe ,接受 Float 並傳回 Float ,不過,Float -> Float 並不單只是宣告,這代表了函式的型態。
一級函式
函式會有型態?這表示 Haskell 之中,函式是個值?是的!或許該拜 JavaScript 熱潮之賜,函式作為一級(First-class)值的概念,不少開發者都很熟悉了,也就是說,跟 1 、3.14 、"Justin" 這些值一樣,函式也可以當作值,將之指定給另一個名稱或傳遞都是可以的,例如:
doubleMe :: Float -> Float
doubleMe x = x + x
doubleThis :: Float -> Float
doubleThis = doubleMe
main = do
putStrLn (show (doubleMe 3.14)) -- 顯示 6.28
putStrLn (show (doubleThis 3.14)) -- 顯示 6.28
簡單!這邊也看到了,如果要撰寫註解,在 Haskell 可使用 -- 。程式中呼叫了 doubleMe 或 doubleThis 的作用是一樣的,都是傳回加倍後 Float ,putStrLn 只接受 String ,如果你直接將 Float 傳給 putStrLn 會發生編譯錯誤。show 函式的型態是 show :: Show a => a -> String ,也就是如果你給他一個具有 Show 這個 Typeclass 規範行為的值,它會傳回一個 String 給你,因此,在這邊將 doubleMe 3.14 的結果傳給 show ,然後才能用 putStrLn 顯示結果。
最低優先權的 $ 函數
如許多程式語言中的慣例,括號可以用來定義運算式的優先順序,因此上頭,你可以看到 putStrLn (show (doubleMe 3.14)) 的寫法,如果你直接寫 putStrLn show doubleMe 3.14 的話會有問題,Haskell 會從左往右執行,putStrLn 看到 show ,會將 show 這個函式當作引數,不過,show 並不接受函式作為引數,因此會編譯錯誤。
只是,像這樣使用括號,形成了巢狀的結果並不好閱讀,你可以試著使用 $ 函式來改善可讀性。$ 是個接受兩個引數的函式,第一個引數是個單參數函式,第二個引數可以是任意值,就像 + 、- 、* 、/ 這些函式一樣,$ 的兩個引數是分別放在其兩側,它會用右邊的值來呼叫左邊的函式。例如:

用右邊的值來呼叫左邊的函式?這算什麼?還不如直接寫 putStrLn "Justin" 就好了!關鍵在於,在 Haskell 中,有一些預定了執行優先順序的函式,例如,* 函式的優先權高於 + 函式,因此 1 + 2 * 3 結果是 7 而不是 9 。
所有函式中,自訂函式的優先執行順序最高,$ 最低。因此,當你撰寫 putStrLn show (1 + 2) 時會出錯,因為 putStrLn 會將 show 當成引數先執行,但是當你撰寫 putStrLn $ show (1 + 2) 時,Haskell 會最後執行 $ 函式,因此就先處理 show (1 + 2) 了。
如果你不想要寫 show (1 + 2) ,進一步地,你也可以寫 show $ 1 + 2 ,再次地,Haskell 會最後執行 $ 函式,因此就先處理 1 + 2 了,因此,putStrLn (show (1 + 2)) ,就可以改為 putStr $ show $ 1 + 2 ,看起來會好讀一些,簡單來說,執行順序變成從右往左了。
因此,putStrLn (show (doubleMe 3.14)) ,可以先在最外層右邊括號旁放上一個 $ 、拿掉括號變成 putStrLn $ show (doubleMe 3.14) ,再來同樣在右邊括號旁放上一個 $ 、拿掉括號變成 putStrLn $ show $ doubleMe 3.14 ,最後,上頭的 main 可以改為:
main = do
putStrLn $ show $ doubleMe 3.14 -- 顯示 6.28
putStrLn $ show $ doubleThis 3.14 -- 顯示 6.28
並不是用了 $ 可讀性就會變好,應適當地搭配 $ 、與括號,找到可讀性的平衡點。
多參數函式
之前說過,Haskell 中, + 、- 、* 、/ 都是函式,他們都是接受兩個引數的函式,那麼,如果要自定義一個兩數相加的函式呢?
在 Haskell 中,定義函式的參數並不需要括號,即使是多參數時也不需要,參數之間也不需要逗號之類的分隔符,例如定義一個兩數相加的函式:

在這邊你看到了 let ,在 GHCI 中你要建立一個名稱,必須使用它,因此,plus 名稱使用了 let 來建立。如〈Haskell Tutorial(3)初探代數與函式〉中談過的,你可以省略函式宣告,Haskell 會試著為你推斷出最適合的型態。正常呼叫方式就是函式名稱後緊接著引數,你也可以用 ` 來括住函式,這樣就可以將第一個引數放在函式之前。
那麼,這個 plus 函式的型態是什麼?使用 :t 來檢驗一下:

Haskell 為你推斷出來的 plus 函式型態,其必須有 Num 這個 Typeclass 的行為,那麼 a -> a -> a 是什麼?如果要簡單解釋,最後一個是傳回型態,之前的就是參數型態了,因此 Num a => a -> a -> a 表示有兩個參數與一個傳回值。而且都是具 Num 行為的型態。
根據以上說明,可以來檢驗一下 + 、- 、* 、/ 各函式的型態:

之前說過,一般雙參數的函式,可以使用 ` 將之轉為引數可置於兩側的形式,相對地,對於 Haskell 本身就定義為引數置於兩側的函式,可以使用括號取得,這就是為何上面要特別用 () 的目的,使用括號也可以將這種 Haskell 本身就定義為引數置於兩側的函式,轉為一般自定義函式的呼叫方式。例如:

不過,方才說過,對於 a -> a -> a ,將最後一個當成傳回值,而前面就是參數,只是個簡單說法,實際上,在 Haskell 中,多參數函式,其實是由多個單參數函式連續呼叫組成,這可以牽扯出部份套用(Partially applied)、Curried 函式,並帶到高階(High-order)函式的使用,我想,這篇文章中的觀念夠多了,剛談到的這幾個名詞,就之後有機會再來談吧!
後續 >> Haskell Tutorial(5)如喝水般自然的高階函式
|