Haskell/資料型別進階
data 宣告的一種特殊情況是列舉型別——一種資料型別,其中所有建構函式都沒有引數。
data Month = January | February | March | April | May | June | July
| August | September | October | November | December
您可以混合帶有引數和不帶引數的建構函式,但結果不再被稱為列舉型別。以下示例不是列舉型別,因為最後一個建構函式需要三個引數。
data Colour = Black | Red | Green | Blue | Cyan
| Yellow | Magenta | White | RGB Int Int Int
正如您將在後面討論類和派生時看到的那樣,在實踐中區分什麼是列舉型別和什麼不是列舉型別是有原因的。
順便說一下,Bool 資料型別是列舉型別。
data Bool = False | True
deriving (Bounded, Enum, Eq, Ord, Read, Show)
考慮一種資料型別,其目的是儲存配置設定。通常,當您從這種型別中提取成員時,您實際上只關心眾多設定中的一兩個。此外,如果許多設定具有相同的型別,您可能會經常發現自己想知道“等等,這是第四個還是第五個元素?” 一種澄清方法是編寫訪問器函式。考慮以下為終端程式製作的配置型別。
data Configuration = Configuration
String -- User name
String -- Local host
String -- Remote host
Bool -- Is guest?
Bool -- Is superuser?
String -- Current directory
String -- Home directory
Integer -- Time connected
deriving (Eq, Show)
然後您可以編寫訪問器函式,例如
getUserName (Configuration un _ _ _ _ _ _ _) = un
getLocalHost (Configuration _ lh _ _ _ _ _ _) = lh
getRemoteHost (Configuration _ _ rh _ _ _ _ _) = rh
getIsGuest (Configuration _ _ _ ig _ _ _ _) = ig
-- And so on...
您還可以編寫更新函式來更新單個元素。當然,如果您在配置中新增或刪除元素,所有這些函式現在都必須採用不同數量的引數。這非常煩人,並且很容易出現錯誤。謝天謝地,有一個解決方案:我們只需在資料型別宣告中為欄位命名,如下所示。
data Configuration = Configuration
{ username :: String
, localHost :: String
, remoteHost :: String
, isGuest :: Bool
, isSuperuser :: Bool
, currentDir :: String
, homeDir :: String
, timeConnected :: Integer
}
這將自動為我們生成以下訪問器函式。
username :: Configuration -> String
localHost :: Configuration -> String
-- etc.
這也為我們提供了一種方便的更新方法。以下是一個“工作目錄後”和“更改目錄”函式的簡短示例,這些函式適用於Configuration。
changeDir :: Configuration -> String -> Configuration
changeDir cfg newDir =
if directoryExists newDir -- make sure the directory exists
then cfg { currentDir = newDir }
else error "Directory does not exist"
postWorkingDir :: Configuration -> String
postWorkingDir cfg = currentDir cfg
因此,一般來說,要將值y中的欄位x更新為z,您需要編寫y { x = z }。您可以更改多個欄位;每個欄位之間用逗號隔開,例如,y {x = z, a = b, c = d }。
注意
熟悉面嚮物件語言的人可能會在聽了所有關於“訪問器函式”和“更新方法”的討論後,將y{x=z}結構視為一個設定器方法,該方法修改了預先存在的y中x的值。它不是那樣的——請記住,在 Haskell 中變數是不可變的。因此,使用上面的示例,如果您執行類似conf2 = changeDir conf1 "/opt/foo/bar"的操作,conf2將被定義為一個Configuration,它與conf1完全相同,除了currentDir為"/opt/foo/bar",但conf1將保持不變。
當然,您可以繼續像以前一樣對Configuration進行模式匹配。命名欄位只是語法糖;您仍然可以編寫類似以下內容的東西。
getUserName (Configuration un _ _ _ _ _ _ _) = un
但沒有必要這樣做。
最後,您可以對命名欄位進行模式匹配,如下所示。
getHostData (Configuration { localHost = lh, remoteHost = rh }) = (lh, rh)
這將變數lh與Configuration中的localHost欄位匹配,並將變數rh與remoteHost欄位匹配。這些匹配當然會成功。您也可以透過在這些位置放置值而不是變數名稱來約束匹配,就像您對標準資料型別所做的那樣。
如果您使用的是 GHC,那麼使用語言擴充套件NamedFieldPuns,還可以使用以下形式。
getHostData (Configuration { localHost, remoteHost }) = (localHost, remoteHost)
它可以與普通形式混合使用,如下所示。
getHostData (Configuration { localHost, remoteHost = rh }) = (localHost, rh)
(要使用此語言擴充套件,請在直譯器中輸入:set -XNamedFieldPuns,或在原始檔開頭使用{-# LANGUAGE NamedFieldPuns #-} 編譯指示,或將-XNamedFieldPuns 命令列標誌傳遞給編譯器。)
您可以使用以下所示的舊方法建立Configuration的值,或者使用命名欄位的型別建立,如第二個定義所示。
initCFG = Configuration "nobody" "nowhere" "nowhere" False False "/" "/" 0
initCFG' = Configuration
{ username = "nobody"
, localHost = "nowhere"
, remoteHost = "nowhere"
, isGuest = False
, isSuperuser = False
, currentDir = "/"
, homeDir = "/"
, timeConnected = 0
}
第一種方式要短得多,但第二種方式要清晰得多。
警告:第二種風格將允許您編寫省略欄位但仍然可以編譯的程式碼,例如。
cfgFoo = Configuration { username = "Foo" }
cfgBar = Configuration { localHost = "Bar", remoteHost = "Baz" }
cfgUndef = Configuration {}
嘗試評估未指定的欄位將導致執行時錯誤!
引數化型別類似於其他語言中的“泛型”或“模板”型別。引數化型別採用一個或多個型別引數。例如,標準 Prelude 型別Maybe 定義如下。
data Maybe a = Nothing | Just a
這意味著型別Maybe 採用型別引數a。您可以使用它來宣告,例如。
lookupBirthday :: [Anniversary] -> String -> Maybe Anniversary
lookupBirthday 函式接受一個生日記錄列表和一個字串,並返回一個Maybe Anniversary。對這種型別的通常解釋是,如果透過字串給出的名稱在週年紀念列表中找到,則結果將是Just 對應的記錄;否則,它將是Nothing。Maybe 是在 Haskell 中表示失敗的最簡單和最常見的方式。它有時也出現在函式引數的型別中,作為使它們可選的一種方式(目的是傳遞Nothing 等同於省略引數)。
您可以以完全相同的方式引數化type 和newtype 宣告。此外,您可以以任意方式組合引數化型別來構造新型別。
我們也可以有多個型別引數。Either 型別的示例如下。
data Either a b = Left a | Right b
例如。
pairOff :: Int -> Either String Int
pairOff people
| people < 0 = Left "Can't pair off negative number of people."
| people > 30 = Left "Too many people for this activity."
| even people = Right (people `div` 2)
| otherwise = Left "Can't pair off an odd number of people."
groupPeople :: Int -> String
groupPeople people =
case pairOff people of
Right groups -> "We have " ++ show groups ++ " group(s)."
Left problem -> "Problem! " ++ problem
在這個例子中,pairOff 指示瞭如果您將一定數量的人分成小組進行活動,您將擁有多少個小組。它還可以讓您知道是否人員過多或是否有人會被排除在外。因此,pairOff 將返回一個表示您將擁有的組數的 Int,或者一個描述您無法建立組的原因的 String。
Haskell 引數化型別的靈活性會導致型別宣告中的錯誤,這些錯誤類似於型別錯誤,只是它們發生在型別宣告中而不是在程式本身中。“型別”中的錯誤被稱為“型別”錯誤。您不會使用型別程式設計:編譯器會自行推斷它們。但是,如果您錯誤地引數化了型別,則編譯器將報告型別錯誤。