跳轉到內容

Haskell/型別宣告

來自華夏公益教科書,開放的書籍,開放的世界

你並不侷限於使用語言預設提供的型別。定義自己的型別有很多好處。

  • 程式碼可以根據要解決的問題進行編寫,從而使程式更容易設計、編寫和理解。
  • 相關資料可以以比簡單地從列表或元組中獲取和設定值更方便和有意義的方式組合在一起。
  • 模式匹配和型別系統可以透過使它們與自定義型別一起工作來充分發揮其作用。

Haskell 有三種基本方法來宣告新的型別。

  • Thedata宣告,定義新的資料型別。
  • Thetype宣告用於型別同義詞,即現有型別的替代名稱。
  • Thenewtype宣告,定義等效於現有型別的新資料型別。

在本章中,我們將學習datatype. 在後面的章節中,我們將討論newtype並瞭解其用途。

data 和建構函式

[edit | edit source]

data用於定義新的資料型別,主要使用現有的資料型別作為構建塊。以下是一個簡單週年紀念列表中元素的資料結構。

data Anniversary = Birthday String Int Int Int       -- name, year, month, day
                 | Wedding String String Int Int Int -- spouse name 1, spouse name 2, year, month, day

這聲明瞭一個新的資料型別Anniversary, 它可以是 Birthday 或 Wedding。Birthday 包含一個字串和三個整數,而 Wedding 包含兩個字串和三個整數。兩種可能性的定義由豎線隔開。註釋向程式碼讀者解釋了這些新型別的預期用途。此外,透過宣告,我們還獲得了兩種用於Anniversary建構函式; 適當地,它們被稱為BirthdayWedding. 這些函式提供了一種構建新的Anniversary.

data宣告定義的型別通常稱為代數資料型別,我們將在後面的章節中進一步討論。

與 Haskell 中的其他內容一樣,第一個字母的大小寫很重要:型別名稱和建構函式必須以大寫字母開頭。除了這個語法細節之外,建構函式的工作方式與我們迄今為止遇到的“傳統”函式幾乎相同。事實上,如果你使用:t在 GHCi 中查詢,例如,Birthday的型別,你將得到

*Main> :t Birthday
Birthday :: String -> Int -> Int -> Int -> Anniversary

這意味著它只是一個函式,它接受一個 String 和三個 Int 作為引數,並計算為一個Anniversary. 這個週年紀念將包含我們傳遞的四個引數,如Birthday建構函式所指定。

呼叫建構函式與呼叫其他函式沒有什麼不同。例如,假設我們有約翰·史密斯,出生於 1968 年 7 月 3 日。

johnSmith :: Anniversary
johnSmith = Birthday "John Smith" 1968 7 3

他於 1987 年 3 月 4 日與簡·史密斯結婚。

smithWedding :: Anniversary
smithWedding = Wedding "John Smith" "Jane Smith" 1987 3 4

這兩個週年紀念可以,例如,放在一個列表中。

anniversariesOfJohnSmith :: [Anniversary]
anniversariesOfJohnSmith = [johnSmith, smithWedding]

或者你也可以在構建列表時直接呼叫建構函式(儘管生成的程式碼看起來有點混亂)。

anniversariesOfJohnSmith = [Birthday "John Smith" 1968 7 3, Wedding "John Smith" "Jane Smith" 1987 3 4]

解構型別

[edit | edit source]

為了使用我們新定義的資料型別,我們必須有一種方法來訪問它們的內容。例如,對上述定義的週年紀念的一個非常基本的操作是提取它們包含的姓名和日期作為 String。因此,我們需要一個showAnniversary函式(為了程式碼清晰起見,我們使用了輔助的showDate函式,但讓我們暫時忽略它)

showDate :: Int -> Int -> Int -> String
showDate y m d = show y ++ "-" ++ show m ++ "-" ++ show d

showAnniversary :: Anniversary -> String

showAnniversary (Birthday name year month day) =
   name ++ " born " ++ showDate year month day

showAnniversary (Wedding name1 name2 year month day) =
   name1 ++ " married " ++ name2 ++ " on " ++ showDate year month day

這個例子展示瞭如何解構我們資料型別中構建的值。showAnniversary接受一個型別為Anniversary的單個引數。但是,我們沒有在定義的左側只提供引數的名稱,而是指定了其中一個建構函式,併為建構函式的每個引數(對應於週年紀念的內容)提供了名稱。描述這種“命名”過程的更正式方法是說我們正在繫結變數。“繫結”是在將變數分配給每個值以使我們能夠在函式定義的右側引用它們的意思。

為了處理“Birthday”和“Wedding”週年紀念,我們需要提供兩個函式定義,每個建構函式一個。當showAnniversary被呼叫時,如果引數是Birthday週年紀念,則使用第一個定義,並將變數name, year, monthday繫結到其內容。如果引數是Wedding週年紀念,則使用第二個定義,並將變數以相同的方式繫結。這種根據建構函式的型別使用函式的不同版本的過程與我們使用case語句或分段定義函式時發生的情況非常類似。

注意,建構函式名稱和繫結變數周圍的括號是必須的;否則,編譯器或直譯器不會將其視為單個引數。此外,重要的是要絕對清楚,括號內的表示式不是對建構函式的呼叫,即使它可能看起來像一個呼叫。

練習

注意:本練習的解決方案在本章的末尾給出,因此我們建議你在檢視解決方案之前嘗試一下。
重新閱讀上面的函式定義。然後仔細觀察showDate輔助函式。我們說它是為了“程式碼清晰”而提供的,但它在使用方式上存在一定笨拙之處。你必須向它傳遞三個單獨的 Int 引數,但這些引數總是作為單個日期的一部分相互關聯。將週年紀念的年份、月份和日期值按不同順序傳遞,或者兩次傳遞月份值並省略日期是沒有意義的。

  • 我們可以使用本章迄今為止看到的內容來減少這種笨拙嗎?
  • 宣告一個Date型別,該型別由三個 Int 組成,分別對應於年份、月份和日期。然後,重寫showDate使其使用新的 Date 資料型別。那麼,需要對showAnniversaryAnniversary進行哪些更改,以便它們可以利用 Date?。

type 用於建立型別同義詞

[edit | edit source]

如本模組簡介中所述,程式碼清晰度是使用自定義型別動機之一。本著這種精神,可以明確地表明週年紀念型別中的 Strings 正在被用作名稱,同時仍然能夠像普通 Strings 一樣操作它們。這就需要一個type宣告

type Name = String

上面的程式碼表示Name現在是String的同義詞。任何接受String的函式現在也將接受Name(反之亦然:接受Name的函式將接受任何String)。type宣告的右側也可以是更復雜的型別。例如,String本身在標準庫中定義為

type String = [Char]

我們可以對我們使用的週年紀念列表做類似的事情。

type AnniversaryBook = [Anniversary]

型別同義詞大多隻是一個便利。它們有助於使型別的作用更加清晰,或者為複雜列表或元組型別提供別名。如何使用型別同義詞在很大程度上取決於個人的判斷。濫用同義詞會導致程式碼混亂(例如,想象一個長時間程式同時使用多個名稱來表示常見的型別,如 Int 或 String)。

將建議的型別同義詞和我們之前練習(*)中提出的Date型別結合起來,我們迄今為止編寫的程式碼如下所示。


((*) **最後一次嘗試練習的機會,不要看答案。**)


type Name = String

data Anniversary = 
   Birthday Name Date
   | Wedding Name Name Date

data Date = Date Int Int Int   -- Year, Month, Day

johnSmith :: Anniversary
johnSmith = Birthday "John Smith" (Date 1968 7 3)

smithWedding :: Anniversary
smithWedding = Wedding "John Smith" "Jane Smith" (Date 1987 3 4)

type AnniversaryBook = [Anniversary]

anniversariesOfJohnSmith :: AnniversaryBook
anniversariesOfJohnSmith = [johnSmith, smithWedding]

showDate :: Date -> String
showDate (Date y m d) = show y ++ "-" ++ show m ++ "-" ++ show d 

showAnniversary :: Anniversary -> String
showAnniversary (Birthday name date) =
   name ++ " born " ++ showDate date
showAnniversary (Wedding name1 name2 date) =
   name1 ++ " married " ++ name2 ++ " on " ++ showDate date

即使在這個簡單的例子中,與只使用 Int、String 和相應的列表相比,在簡單性和清晰度方面也有顯著的提升。

注意,Date型別有一個建構函式,該建構函式也稱為Date. 這完全有效,實際上,當只有一個建構函式時,將建構函式命名為與型別相同的名稱是一種很好的做法,因為它是一種使函式作用顯而易見的方法。

注意

在這些初始示例之後,使用建構函式的機制可能看起來有點笨拙,特別是如果你熟悉其他語言中的類似特性。有一些語法結構可以使處理建構函式更加方便。我們將在稍後返回到建構函式和資料型別的主題,以詳細地探索它們,屆時將介紹這些結構。


華夏公益教科書