Haskell/类型声明
您不限于仅使用语言默认提供的类型。定义自己的类型有很多好处
- 代码可以根据要解决的问题进行编写,使程序更容易设计、编写和理解。
- 相关数据可以以比简单地从列表或元组中获取值更方便和更有意义的方式组合在一起。
- 模式匹配和类型系统可以通过让它们与自定义类型一起工作来最大程度地利用它们。
Haskell 有三种基本方法来声明新的类型
- Thedata声明,它定义新的数据类型。
- Thetype用于类型别名,即现有类型的替代名称。
- Thenewtype声明,它定义等同于现有类型的新数据类型。
在本章中,我们将学习data和type. 在后面的章节中,我们将讨论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;恰如其分,它们被称为Birthday和Wedding. 这些函数提供了一种构建新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, month和day绑定到其内容。如果参数是WeddingAnniversary,则使用第二个定义,并且变量以相同的方式绑定。这种根据构造函数的类型使用不同版本的函数的过程与我们使用case语句或逐段定义函数时发生的情况非常类似。
请注意,构造函数名称和绑定变量周围的括号是必需的;否则编译器或解释器不会将它们视为单个参数。此外,重要的是要绝对清楚,括号内的表达式 不是 对构造函数的调用,即使它看起来像这样。
练习 |
---|
注意:本练习的解答在本章的最后给出,因此我们建议您在看到解答之前尝试一下。
|
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也是。这完全有效,事实上,当只有一个构造函数时,给构造函数命名为与类型相同的名称是一种很好的做法,因为它是一种简单的方法,可以使函数的作用显而易见。
注意
在这些初始示例之后,使用构造函数的机制可能看起来有点笨拙,尤其是如果您熟悉其他语言中的类似功能。有一些语法结构可以使处理构造函数更方便。这些将在以后讨论,当我们回到构造函数和数据类型主题以详细探索它们时。