到目前為止,基本上你已經知道如何定義一個函式,大致上也瞭解如何宣告函式的型態,例如,在〈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)函式的使用,我想,這篇文章中的觀念夠多了,剛談到的這幾個名詞,就之後有機會再來談吧!