跳轉到內容

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}結構視為一個設定器方法,該方法修改了預先存在的yx的值。它不是那樣的——請記住,在 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)

這將變數lhConfiguration中的localHost欄位匹配,並將變數rhremoteHost欄位匹配。這些匹配當然會成功。您也可以透過在這些位置放置值而不是變數名稱來約束匹配,就像您對標準資料型別所做的那樣。

如果您使用的是 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 對應的記錄;否則,它將是NothingMaybe 是在 Haskell 中表示失敗的最簡單和最常見的方式。它有時也出現在函式引數的型別中,作為使它們可選的一種方式(目的是傳遞Nothing 等同於省略引數)。

您可以以完全相同的方式引數化typenewtype 宣告。此外,您可以以任意方式組合引數化型別來構造新型別。

多個型別引數

[編輯 | 編輯原始碼]

我們也可以有多個型別引數。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 引數化型別的靈活性會導致型別宣告中的錯誤,這些錯誤類似於型別錯誤,只是它們發生在型別宣告中而不是在程式本身中。“型別”中的錯誤被稱為“型別”錯誤。您不會使用型別程式設計:編譯器會自行推斷它們。但是,如果您錯誤地引數化了型別,則編譯器將報告型別錯誤。

華夏公益教科書