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]为了使用我们新定义的数据类型,我们必须有一种方法来访问它们的内容。例如,对上述定义的周年纪念的一个非常基本的操作是提取它们包含的姓名和日期作为 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, month和day绑定到其内容。如果参数是Wedding周年纪念,则使用第二个定义,并将变量以相同的方式绑定。这种根据构造函数的类型使用函数的不同版本的过程与我们使用case语句或分段定义函数时发生的情况非常类似。
注意,构造函数名称和绑定变量周围的括号是必须的;否则,编译器或解释器不会将其视为单个参数。此外,重要的是要绝对清楚,括号内的表达式不是对构造函数的调用,即使它可能看起来像一个调用。
练习 |
---|
注意:本练习的解决方案在本章的末尾给出,因此我们建议你在查看解决方案之前尝试一下。
|
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. 这完全有效,实际上,当只有一个构造函数时,将构造函数命名为与类型相同的名称是一种很好的做法,因为它是一种使函数作用显而易见的方法。
注意
在这些初始示例之后,使用构造函数的机制可能看起来有点笨拙,特别是如果你熟悉其他语言中的类似特性。有一些语法结构可以使处理构造函数更加方便。我们将在稍后返回到构造函数和数据类型的主题,以详细地探索它们,届时将介绍这些结构。