使用 data 定义类型
让我们来学习如何定义自己的 Haskell 数据类型。
到目前为止,我们一直只使用 Haskell 的预定义类型,例如数字、字符串和列表。在本章中,我们将学习如何创建自己的数据类型。
使用自定义类型可以提高代码的可读性和安全性。为了说明这一点,让我们回顾一下之前课程中编写的 perimeter 函数,该函数用于计算矩形的周长。
1perimeter :: (Double, Double) -> Double 2perimeter (a, b) = 2 * a + 2 * b
这个函数接受两个 Double 类型的输入。但从类型定义中无法明确看出这两个 Double 值是矩形的两条边。此外,我们可以随意传入任意两个 Double 值调用该函数,即使它们之间没有任何关联。
这两个问题都可以通过定义一个自定义的 Rectangle 类型来解决,并相应地定义:
1perimeter :: Rectangle -> Double 2perimeter (Rectangle width height) = 2 * width + 2 * height
现在可以清楚地知道 perimeter 是作用于矩形的,而且编译器能够确保它只被应用于矩形类型。接下来我们将学习如何创建这样的类型。
使用 data 创建类型
在 Haskell 中,可以通过 data 关键字创建自定义类型。举个例子,我们来创建一种表示不同几何图形的数据类型。
1data Geometry = Rectangle Double Double | Square Double | Circle Double
我们的 Geometry 数据类型允许创建三种不同的形状(由管道符 | 分隔):矩形、正方形和圆形。矩形由两个 Double 值描述(其宽和高),正方形由一个 Double 值描述(其边长),圆形也由一个 Double 值描述(其半径)。
这个类型名为 Geometry,而 Rectangle、Square 和 Circle 是它的三个数据构造器。类型名和构造器名都必须以大写字母开头。
下面是一个运行 ghci 的终端,其中已经定义了 Geometry 类型。你可以使用它尝试以下命令。
我们可以使用构造器来创建 Geometry 值。
1*Geometry> s1 = Rectangle 3 5 :: Geometry 2*Geometry> s2 = Square 4 :: Geometry 3*Geometry> s3 = Circle 7 :: Geometry
同时,这些数据构造器在模式匹配时也可以作为解构器使用。以下是针对这些形状的 perimeter 函数:
1perimeter :: Geometry -> Double 2perimeter (Rectangle width height) = 2 * width + 2 * height 3perimeter (Square width) = width * 4 4perimeter (Circle radius) = 2 * pi * radius
我们的 data 声明自动为我们生成了模式 Rectangle _ _、Square _ 和 Circle _,我们可以用它们来匹配形状,并将其中包含的值绑定到变量上。
1*Geometry> perimeter (Circle 5) 231.41592653589793
你也可以在上面的终端中尝试运行 perimeter 函数。
显示 Geometry 值
下面是另一个示例函数,它会解构形状并重新构造它们以实现放大效果。
1enlarge :: Geometry -> Double -> Geometry 2enlarge (Rectangle a b) d = Rectangle (d*a) (d*b) 3enlarge (Square a) d = Square (d*a) 4enlarge (Circle r) d = Circle (r*d)
然而,当我们尝试在 ghci 中执行此函数时,会遇到错误。
1Geometry*> enlarge (Square 2) 2 2 3<interactive>:17:1: error: 4 • No instance for (Show Geometry) arising from a use of ‘print’ 5 • In a stmt of an interactive GHCi command: print it
问题并不出在 enlarge 函数本身,而是在函数执行后,ghci 尝试将结果 Geometry 值(如 Square 4)打印到屏幕上时发生的。
为此,ghci 会调用 show 函数将形状转换为字符串。但我们的新类型还没有定义 show 函数。
幸运的是,有一个简单的解决方案。我们可以告诉 ghc 自动为我们的自定义数据类型生成 show 函数。只需在 data 声明中添加 deriving (Show) 即可。
1data Geometry = Rectangle Double Double | Square Double | Circle Double deriving (Show)
现在 ghci 就能正确打印形状了。
1*Geometry> enlarge (Square 2) 2 2Square 4.0
show 的默认实现会打印构造器名称,然后递归地对内部值调用 show。
我们稍后会进一步学习 deriving 机制,以及如何手动实现自定义的 show 函数。
构造器作为函数
我们还可以把像 Rectangle 和 Square 这样的构造器看作是接收 Double 值并返回 Geometry 值的函数。事实上,Haskell 编译器正是这样处理它们的。
1*Geometry> :t Rectangle 2Rectangle :: Double -> Double -> Geometry 3 4*Geometry> :t Square 5Square :: Double -> Geometry
当我们写 Square 5.0 时,实际上是在像调用函数一样使用数据构造器,将参数 5.0 应用于它,从而得到一个 Geometry 值。
由于数据构造器可以像函数一样使用,因此它们也可以作为高阶函数的参数传递。
1*Geometry> map Square [1.0, 3.2] 2[Square 1.0, Square 3.2]
枚举类型
data 声明中的构造器不需要包含嵌套的值类型;它们也可以独立存在。在这种情况下,data 定义的类型可以像其他编程语言中的 枚举 类型一样使用。下面是一个动物类型的例子。
1data Animal = Cat 2 | Dog 3 | Mouse 4 | Elephant
使用这种枚举类型表示动物,而不是用字符串存储动物名称,其优势在于:每当一个 Animal 类型的值被使用时,它一定是 data 声明中定义的四个有效动物之一。
Haskell 的 Bool 类型也是一个枚举类型的例子。它的定义如下:
1data Bool = True | False