Haskell Tutorial(4)這裏,那裏,到處都是函式


到目前為止,基本上你已經知道如何定義一個函式,大致上也瞭解如何宣告函式的型態,例如,在〈Haskell Tutorial(2)一絲不苟的型態系統〉中,定義過一個 doubleMe 函式:

doubleMe :: Float -> Float
doubleMe x = x + x

根據 doubleMe :: Float -> Float,你知道函式名稱是 doubleMe,接受 Float 並傳回 Float,不過,Float -> Float 並不單只是宣告,這代表了函式的型態。

一級函式

函式會有型態?這表示 Haskell 之中,函式是個值?是的!或許該拜 JavaScript 熱潮之賜,函式作為一級(First-class)值的概念,不少開發者都很熟悉了,也就是說,跟 13.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 可使用 --。程式中呼叫了 doubleMedoubleThis 的作用是一樣的,都是傳回加倍後 FloatputStrLn 只接受 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 來檢驗一下:

plus 函式的型態

Haskell 為你推斷出來的 plus 函式型態,其必須有 Num 這個 Typeclass 的行為,那麼 a -> a -> a 是什麼?如果要簡單解釋,最後一個是傳回型態,之前的就是參數型態了,因此 Num a => a -> a -> a 表示有兩個參數與一個傳回值。而且都是具 Num 行為的型態。

根據以上說明,可以來檢驗一下 +-*/ 各函式的型態:

+、-、*、/ 的型態

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

() 的使用

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