Haskell Tutorial(14)減輕型態負擔的型態參數


你設計了一個 swapInt 函式,可以將傳入的 Tuple 中兩個 Int 元素對調:

swapInt :: (Int, Int) -> (Int, Int)
swapInt (x, y) = (y, x)

這邊在 swapInt 上使用了 Tuple 的模式比對,這個 swapInt 接受兩個 Int,不過,實際上,也會需要可對調 Float 元素的版本:

swapFloat :: (Float, Float) -> (Float, Float)
swapFloat (x, y) = (y, x)

馬上就可以看出,這兩個函式定義除了名稱與型態之外,其餘是相同的,如果需要更多不同型態的 swap 版本,也許 Tuple 兩元素也想要不同,需求就出現了 … 如果型態也可以參數化,也就是根據實際傳入的引數型態來決定 xy 的型態就好了!

型態參數化

在 Haskell 中函式設計時可不理會參數實際型態,函式的實作版本只有一個,例如,上面的需求,可以實作以下的 swap 函式來解決:

swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)

在函式的型態定義 swap :: (a, b) -> (b, a) 中,ab 取代了實際的型態宣告,表示各可以是不同型態,實際型態將由編譯器推斷或自行指定而決定,例如,swap (10, 3.14) 的話,a 的型態會是 Integer,而 b 型態會是 Double,傳回值型態就是 (Double, Integer),如果傳入 swap (3.14::Float, 100::Int) 的話,傳回型態就會是 (Int, Float)

由於 ab 型態可以自行指定,如同函式的參數可以自行指定引數一樣,ab 被稱之為型態參數(Type parameter),型態可以參數化,開發者在設計函式時就減輕了為各種不同型態建立不同版本函式的負擔,可以使用同一個介面來處理多種不同型態的需求,也就是多型(Polymorphism)的一種實現,稱之為參數多型(Parametric polymorphism),因為這在 Haskell 是自然且常見的實現,因此在 Haskell 中都直接稱多型。

Typeclass 型態約束

如果你的 swap 只想適用所有數字,像是整數、浮點數、分數等,但不適用布林值等其他數怎麼辦?在〈Haskell Tutorial(2)一絲不苟的型態系統〉中談過 Typeclass,具有某個 Typeclass 行為的型態,必須實現該 Typeclass 規範的行為,規範整數、浮點數、分數行為的 Typeclass 是 Num,在定義型態參數時,也可以使用 Typeclass 來約束實際可用的型態。例如:

swap :: (Num a, Num b) => (a, b) -> (b, a)
swap (x, y) = (y, x)

(Num a, Num b) => 約束了 ab 可用的型態必須具有 Num 的行為,因此,整數、浮點數、分數等能夠使用 swap 函式,而其他型態不行:

型態約束

如果只有一個型態約束,那麼可以不使用括號,像是上例中,其實 ab 都約束為 Num,那麼直接這麼定義就可以了:

swap :: Num a => (a, a) -> (a, a)
swap (x, y) = (y, x)

你也可以只約束其中一個型態,例如:

swap :: Num a => (a, b) -> (b, a)
swap (x, y) = (y, x)

這麼一來,傳入的 Tuple 首項一定得是 Num,第二項隨意。現在回頭去看看〈Haskell Tutorial(2)一絲不苟的型態系統〉中,一些檢驗函式的型態,應該就可以更瞭解型態宣告的意義了。例如:

doubleMe x = x + x

這個函式沒有定義型態,使用 :t 檢驗看看,編譯器為你推斷為何種型態?

型態約束

Haskell 的型態推斷,會試著使用最寬鬆適用的型態,在這邊可以看到,凡是 Num 都會有 + 的行為,因此,推斷出 doubleMe 的型態為 Num a => a -> a

自訂型態時的型態參數

上面的例子一直使用 Tuple 舉例,之前有幾個篇幅談過,Tuple 就像個沒有名稱的型態,既然可以 Tuple 上使用型態參數,那能不能在自訂型態時也使用型態參數?當然,這時型態的實例多半作為一種容器。

舉例來說,你應該經常遇到查詢結果沒有值的情況,例如,某個 List 中沒有指定的元素,這時該傳回什麼呢?在 Java 這類有 null 值的語言中,經常會在沒有值時傳回 null,因為 null 可以作為任何型態的值,然而在 Haskell 中可沒那麼簡單!

來重新想想需求,你的查詢可能沒有值,可不可以定義一個 Nothing 來專門代表沒有值呢?

data Nothing = Nothing

這麼一來,為了讓函式傳回 Nothing,函式的型態宣告傳回值部份就必須是 Nothing 型態,那麼有值的時候怎麼辦?例如,某個 Int 的 List 中存在想查詢的值,可是函式的型態宣告傳回值宣告為 Nothing 型態了,就不能傳回 Int 了!既然值可能有也可能沒有,那就定義為 Maybe 型態吧!Nothing 只是 Maybe 的一個實例,至於值就包裝為 Maybe 的一個實例好了:

data Maybe a = Nothing | Just a

Maybe 型態的 a 表示型態參數,Maybe 現在是個型態建構式,用來建立具體型態,使用 Just 10 建構出來的值,具體型態會是 Maybe Integer,使用 Just "Irene" 建構出來的值,具體型態會是 Maybe [Char](也就是 Maybe String)。

實際上,Haskell 中確實有內建 Maybe,因此直接用就可以了:

Maybe

在上例中,Nothing 的型態由於沒有進一步資訊,因而這邊推斷 Maybe a

來看看這個 Maybe 的實際應用:

import System.IO

password :: String -> Maybe String
password person = lookupUsers [("Justin", "1234"), ("Monica", "4321")]
    where
        lookupUsers [] = Nothing
        lookupUsers ((name, passwd):xs) =
            if name == person then Just passwd
                              else lookupUsers xs

main = do putStr "請輸入你的名稱:"
          hFlush stdout
          person <- getLine
          putStrLn $ case password person of Nothing     -> "查無此人"
                                             Just passwd -> passwd

password 函式中,如果查詢到對應的密碼,就傳回 Maybe String 實例,例如 Just "1234",如果沒有對應的密碼,就傳回 Nothing,此時 Nothing 會被推斷為 Maybe String,這樣型態就符合函式的宣告了。Maybe 實例經常搭配模式比對,就像上面的 main 示範。

回想一下,在〈Haskell Tutorial(13)正式入門代數資料型態〉中,自定義了 List 型態:

data List = Empty | Con Int List

這個 List 只能裝 Int,你想要的 List 的元素可以是任意型態,只要所有元素是相同元素,這時也可以如下定義:

data List a = Empty | Con a (List a)

來出個題目好了,在剛剛的 password 例子中,其實是使用了 [("Justin", "1234"), ("Monica", "4321")] 來模擬鍵值對照的 Map 結構,你可以自定義一個專用的 Map,並定義一個 fromList 方法,來方便的建立 Map 實例,一個 findValue 方法,可以指定鍵來尋找值,像是具有以下的效果?

Map