減輕型態負擔的型態參數
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 的兩個元素也想要能有不同的型態,需求就出現了,如果型態也可以參數化,也就是根據實際傳入的引數型態來決定 x
、y
的型態就好了!
型態參數化
上面的需求,可以實作以下的 swap
函式來解決:
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
在函式的型態定義 swap :: (a, b) -> (b, a)
中,a
、b
取代了實際的型態宣告,表示可以是不同型態,實際型態將由編譯器推斷或自行指定而決定,例如,swap (10, 3.14)
的話,a
的型態會是 Num
,而 b
型態會是 Fractional
,傳回值型態就是 (Fractional, Num)
,如果傳入 swap (3.14::Float, 100::Int)
的話,傳回型態就會是 (Int, Float)
。
由於 a
、b
型態如同函式的參數可以自行指定引數一樣,a
、b
被稱為型態參數(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) =>
約束了 a
、b
可用的型態必須具有 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>
如果只有一個型態約束,那麼可以不使用括號,像是上例中,其實 a
、b
都約束為 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>