Haskell Tutorial(16)Record 語法、Type 同義詞


假設你想定義一個 Rectangle 型態,可定義左上原點座標與寬長:

data Rectangle = Rect Int Int Int Int deriving Show

嗯?四個 Int 中哪些是原點 x、y 座標?哪些又是寬高 width、height 呢?也許你可以建立一些函式來解決這個問題:

xOf (Rect x _ _ _) = x
yOf (Rect _ y _ _) = y
widthOf (Rect _ _ w _) = w
heightOf (Rect _ _ _ h) = h

這麼一來,如果有個 Rectangle 型態的值 rect,就可以分別使用 xOf rectwidthOf rect 等來取得座標或寬長等值,不過,在建立 Rectangle 的值時,你還是得記得各項的順序,像是 Rect 10 5 20 30 這樣的順序,並不是很方便。

使用 Record 記錄各項名稱

Haskell 提供了一個 Record 語法,可以讓你指定各項名稱:

data Rectangle = Rect { x :: Int
                      , y :: Int
                      , width :: Int
                      , height :: Int } deriving Show

這麼做的好處有幾個,從型態定義上,可清楚地知道各項之意義,如果你使用 deriving 自動衍生自 Show,那麼產生的字串描述中,也會包括 Record 語法中指定的各項名稱:

Record 記錄各項名稱

而且,現在建構 Rectangle 的值時,不一定得按照順序了,你可以指定項目名稱的值各是為何就可以了:

Record 記錄各項名稱

實際上,Haskell 會使用你指定的項目名稱,產生各個函式,例如,這邊就產生了 xywidthheight 四個函式,可以讓你指定 Rectangle 的值,分別當中取得各項的值:

Record 記錄各項名稱

你還是可以使用 Rect 10 20 15 30 的方式來建立 Rectangle 的值,因為值構造式是個函式,也可以使用 Rect 10 20 這樣的方式來做部份套用,不過,使用 Record 語法建立值時,每個項都必須指定。

Record 記錄各項名稱

建立 Type 同義詞

如果你寫了個 allToUpper 函式,可以將指定的小寫字串清單,全部轉為大寫的字串清單:

import Data.Char

allToUpper :: [[Char]] -> [[Char]]
allToUpper xs = [map toUpper x | x <- xs]

在之前就談過,字串實際上是字元清單,因此對於一個字串清單,它的型態其實是 [[Char]],所以在 allToUpper 的函式宣告上,可以看到 [[Char]] -> [[Char]],這並不好閱讀,你可以改定義為:

import Data.Char

allToUpper :: [String] -> [String]
allToUpper xs = [map toUpper x | x <- xs]

這顯然容易閱讀的多,可以這麼定義的原因在於,Haskell 使用了 type 關鍵字定義了 String[Char] 的同義詞:

type String = [Char]

如果使用了同義詞來定義函式型態,使用 :t 來測試 allToUpper 時,結果會顯示 allToUpper :: [String] -> [String] 而不是一開始看到的 allToUpper :: [[Char]] -> [[Char]]

你可以為任何具體型態定義同義詞,例如,若有個函式可接受 URL 對應清單進行處理,像是 [("GET /books", "books/index"), ("POST /books", "/books/create")],那麼函式型態宣告時的參數定義會像是 [([Char], [Char])] -> SomeType,你可以改為 [(String, String)],或者進一步定義同義詞:

type UrlMappingLt = [([Char], [Char])]

這麼一來,你的函式型態宣告就會像是 UrlMappingLt -> SomeType,較為簡潔一些。

具型態參數的同義詞

除了為具體型態建立同義詞之外,定義同義詞時也可以有型態參數,還記得〈Haskell Tutorial(14)減輕型態負擔的型態參數〉的最後,我出了一個題目嗎?不知道你有沒有完成?可以實作的方式之一是:

data Map k v = Empty | Cm (k, v) (Map k v) 

fromList :: [(k, v)] -> Map k v
fromList [] = Empty
fromList (x:xs) = Cm x (fromList xs)

findValue :: (Eq k) => k -> Map k v -> Maybe v
findValue key Empty = Nothing
findValue key (Cm (k, v) xm) =
    if key == k then Just v else findValue key xm

顯然地,這只是在模仿 List 的定義,並不是實際上 Haskell 中 Data.Map 模組中的定義,這麼實作是過份簡化了,不過作為一個練習是夠了。

在上頭的練習中,fromList 型態宣告是 [(k, v)] -> Map k v,如果你定義:

type PairLt k v = [(k, v)]

那麼 fromList 的函式宣告,就可以改為 PairLt k v -> Map k v

fromList :: PairLt k v -> Map k v
fromList [] = Empty
fromList (x:xs) = Cm x (fromList xs)

你也可以在定義同義詞時,部份套用型態參數,例如,也許有某個函式:

lookUpByName :: [Char] -> [([Char], v)] -> Maybe v
lookUpByName n ((name, value):xs) =
    if n == name then Just value else lookUpByName n xs

這種情況下透過同義詞,可以讓 [Char] -> [([Char], v)] -> Maybe v 變得簡潔一些:

type StringKeyPairLt v = [([Char], v)]

lookUpByName :: String -> StringKeyPairLt v -> Maybe v
lookUpByName n ((name, value):xs) =
    if n == name then Just value else lookUpByName n xs

那麼,如果定義了同義詞,想要知道它原等同哪個型態定義怎麼辦?你可以使用 :info 來得知:

查詢同義詞