跳轉至內容

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]

要使用我們新的資料型別,我們必須有一種方法來訪問它們的內容。例如,對上面定義的紀念日的一個非常基本的操作將是將它們包含的姓名和日期作為字串提取出來。所以我們需要一個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的單個引數。但是,在定義的左側不只是提供引數的名稱,而是指定了建構函式之一,併為建構函式的每個引數(對應於 Anniversary 的內容)提供了名稱。描述這種“命名”過程的更正式方法是說我們正在 繫結變數。 “繫結”在將變數分配給每個值以便我們可以在函式定義的右側引用它們的意思上使用。

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

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

練習

注意:本練習的解答在本章的最後給出,因此我們建議您在看到解答之前嘗試一下。
重新閱讀上面的函式定義。然後仔細看看showDate輔助函式。我們說它是為了“程式碼清晰起見”提供的,但它在使用方式上有一定的笨拙。您必須向它傳遞三個單獨的 Int 引數,但這些引數總是作為單個日期的一部分相互關聯。將 Anniversary 的年份、月份和日期值以不同的順序傳遞,或者兩次傳遞月份值並省略日期,這樣做毫無意義。

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

type 用於建立類型別名

[edit | edit source]

如本模組介紹中所述,程式碼清晰是使用自定義型別的原因之一。本著這種精神,明確 Anniversary 型別中的字串用作 名稱,同時仍然能夠像普通字串一樣操作它們,這將是一件好事。這需要一個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也是。這完全有效,事實上,當只有一個建構函式時,給建構函式命名為與型別相同的名稱是一種很好的做法,因為它是一種簡單的方法,可以使函式的作用顯而易見。

注意

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


華夏公益教科書