跳转至内容

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

注意

在这些初始示例之后,使用构造函数的机制可能看起来有点笨拙,尤其是如果您熟悉其他语言中的类似功能。有一些语法结构可以使处理构造函数更方便。这些将在以后讨论,当我们回到构造函数和数据类型主题以详细探索它们时。


华夏公益教科书