跳转到内容

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. 这完全有效,实际上,当只有一个构造函数时,将构造函数命名为与类型相同的名称是一种很好的做法,因为它是一种使函数作用显而易见的方法。

注意

在这些初始示例之后,使用构造函数的机制可能看起来有点笨拙,特别是如果你熟悉其他语言中的类似特性。有一些语法结构可以使处理构造函数更加方便。我们将在稍后返回到构造函数和数据类型的主题,以详细地探索它们,届时将介绍这些结构。


华夏公益教科书