減輕型態負擔的型態參數

February 6, 2022

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

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

這邊使用了 tuple 的模式比對;你也需要可對調 Float 元素的版本:

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

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

型態參數化

上面的需求,可以實作以下的 swap 函式來解決:

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

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

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

型態類別約束

如果你的 swap 只想適用所有數字,但不適用布林值等其他數怎麼辦?在〈型態系統入門〉談過型態類別(Typeclass),具有某個型態類別行為的型態,必須實現該型態類別規範的行為,規範數字行為的型態類別是 Num,在定義型態參數時,也可以使用型態類別來約束實際可用的型態。例如:

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

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

ghci> swap (10, 2.1)
(2.1,10)
ghci> swap True 10  

<interactive>:6:1: error:
    ‧ Couldn't match expected type: t0 -> t
                  with actual type: (b0, a0)
    ‧ The function ‘swap’ is applied to two value arguments,
        but its type ‘(a0, b0) -> (b0, a0)’ has only one
      In the expression: swap True 10
      In an equation for ‘it’: it = swap True 10
    ‧ Relevant bindings include it :: t (bound at <interactive>:6:1)

<interactive>:6:6: error:
    ‧ Couldn't match expected type ‘(a0, b0)’ with actual type ‘Bool’
    ‧ In the first argument of ‘swap’, namely ‘True’
      In the expression: swap True 10
      In an equation for ‘it’: it = swap True 10
ghci>

如果只有一個型態約束,那麼可以不使用括號,像是上例中,其實 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,第二項隨意。現在回頭去看看〈型態系統入門〉中,一些檢驗函式的型態,應該就可以更瞭解型態宣告的意義了。

自訂型態時的型態參數

上面的例子一直使用 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 建構出來的值,具體型態會是 Num a => Maybe a,也就是說 a 會具有 Num 行為,如果你想指定為 Maybe Int,可以使用 (Just 10)::Maybe Int;使用 "Just "Irene" 建構出來的值,具體型態則會是 Maybe String

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

ghci> let x = Just 10
ghci> :t x
x :: Num a => Maybe a
ghci> let y = Nothing
ghci> :t y 
y :: Maybe a
ghci>

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

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

import System.IO

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

answer :: Maybe String -> String                  
answer Nothing = "查無此人"
answer (Just passwd) = passwd

main = do 
    putStr "請輸入你的名稱:"
    hFlush stdout
    username <- getLine
    putStrLn $ (answer . password) username

password 函式中,如果查詢到對應的密碼,就傳回 Maybe String 實例,例如 Just "1234",如果沒有對應的密碼,就傳回 Nothing

回想一下,在〈結合 sum 與 product 型態〉中,自定義了 List 型態:

data List = Empty | Con Int List deriving Show

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

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

這麼一來,List 可以裝的就不只是 Int 了:

ghci> let lt1 = Con "Justin" Empty
ghci> let lt2 = Con "Monica" lt1  
ghci> let lt3 = Con "Irene" lt2 
ghci> lt3
Con "Irene" (Con "Monica" (Con "Justin" Empty))
ghci> :t lt3
lt3 :: List String
ghci>

分享到 LinkedIn 分享到 Facebook 分享到 Twitter