文章
Get Programming with Haskell-14
你将更深入地了解如何实现已有的类型类。这将使你能够为你自定义的新类型实现类型类,从而利用大量现有的函数功能。
目录
在这里,我们为 `SixSidedDie` 类型定义了 `show` 方法,并为每个数据构造器(S1 到 S6)返回了相应的英文单词表示。这样,当你在 GHCi 中打印 `SixSidedDie` 类型的值时,它将输出更具可读性的英文单词,而不是数据构造器的名字。

## 3. 类型类(type classes)和多态性(polymorphism)
一个可能会出现的问题是,为什么必须这样定义`show`?为什么需要声明一个类型类的实例?令人惊讶的是,如果你移除早期的实例声明,以下代码仍然可以正常编译。
```haskell
show :: SixSidedDie -> String
show S1 = "one"
show S2 = "two"
show S3 = "three"
show S4 = "four"
show S5 = "five"
show S6 = "six"
但是,如果你将这段代码加载到 GHCi 中,你会遇到两个问题。首先,GHCi 不再默认打印你的数据构造函数。其次,即使你手动使用 show,也会出现一个错误:
“Ambiguous occurrence 'show'”
你还没有学习 Haskell 的模块系统,但问题在于你刚才为 show 编写的定义与类型类中定义的另一个 show 冲突了。当你创建一个 TwoSidedDie 类型并尝试为其编写 show 时,你可以看到真正的问题。
data TwoSideDie = One | Two
show :: TwoSideDie -> String
show One = "one"
show Two = "two"
编译错误,我们定义了多个”show”
问题在于,默认情况下,你希望根据使用的类型为 show 提供不同的行为。你在这里需要的就是多态性。多态性意味着同一个函数根据它处理的数据类型表现出不同的行为。多态性在面向对象编程中非常重要,在 Haskell 中也是如此。在面向对象编程中,与 show 等效的就是 toString 方法,它是所有可以转化为字符串的类共有的方法。类型类是 Haskell 中实现多态性的方式,如图所示。

4. 默认实现和最小完整定义#
现在你可以为你的 SixSidedDie 类型生成有趣的字符串了,接下来,你可能需要判断两个骰子是否相同。这意味着你需要实现 Eq 类。这也很有用,因为 Eq 是 Ord 的超类。在上一章中简单提到过这个关系,但没有给它命名。说 Eq 是 Ord 的超类意味着每一个 Ord 的实例必须也是 Eq 的实例。最终,你希望能够比较 SixSidedDie 的数据构造函数,这就需要实现 Ord,所以首先你需要实现 Eq。 在 GHCi 中使用 :info 命令,你可以查看 Eq 类的定义:
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
你只需要实现两个方法:等于方法 (==) 和不等于方法 (/=)。考虑到 Haskell 到目前为止的智能,这似乎是多余的工作。毕竟,如果你知道 (==) 的定义,那么 (/=) 的定义就应该是 (==) 的反义词。当然,可能有一些例外情况,但在绝大多数情况下,如果你知道其中一个,就可以推导出另一个。 事实证明,Haskell 足够聪明,能够自动推导这一点。类型类可以为方法提供默认实现。如果你定义了 (==),Haskell 就可以自动推导出 (/=) 的含义,而无需额外帮助。
instance Eq SixSidedDie where
(==) S6 S6 = True
(==) S5 S5 = True
(==) S4 S4 = True
(==) S3 S3 = True
(==) S2 S2 = True
(==) S1 S1 = True
(==) _ _ = False
这很有用,但你怎么知道究竟需要实现哪些方法呢?:info 命令是一个很好的信息来源,可以让你随时查看,但它并不是完整的文档。一个更为详细的信息来源是 Hackage,Haskell 的中央包库。Hackage 可以在网上找到,网址是 https://hackage.haskell.org。如果你访问 Eq 在 Hackage 上的页面 (https://hackage.haskell.org/package/base/docs/Data-Eq.html),你可以获得更多关于 Eq 的信息(可能比你想要的还要多!)。对于我们的目的,最重要的部分是一个叫做“Minimum complete definition”(最小完整定义) 的部分。对于 Eq,你会看到如下内容:
(==) | (/=)
这更有帮助!要实现 Eq 类型类,你只需要定义 (==) 或 (/=) 之一。就像在数据声明中,| 代表“或者”。如果你提供了这两个选项中的任何一个,Haskell 就能为你推导出剩下的部分。
**Hackage **和 Hoogle 尽管 Hackage 是 Haskell 信息的中央仓库,但你可能会发现搜索特定类型有点麻烦。为了解决这个问题,Hackage 可以通过一个非常棒的接口进行搜索,这个接口叫做 Hoogle。Hoogle 可以在 https://hoogle.haskell.org/ 找到。Hoogle 允许你通过类型和类型签名进行搜索。例如,如果你搜索
a -> String,你将获得 show 函数以及其他各种函数的结果。仅仅是 Hoogle 就足以让你爱上 Haskell 的类型系统。
5. 实现 Ord#
骰子最重要的特性之一是它们的各个面之间是有顺序的。Ord 类型类定义了一些用于比较类型的有用函数:
class Eq a => Ord a where
compare :: a -> a -> Ordering
(<) :: a -> a -> Bool
(<=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(>=) :: a -> a -> Bool
max :: a -> a -> a
min :: a -> a -> a
幸运的是,在 Hackage 上你可以发现,实现 Ord 类型类时只需要定义 compare 方法。
compare 方法接受你的类型的两个值,并返回一个 Ordering 类型的结果。
你在开始章节中学习 sort 函数时曾简要见过 Ordering 类型。它和 Bool 类似,但有三个数据构造器。下面是它的定义:
data Ordering = LT | EQ | GT
下面是 compare 的一个部分定义示例:
compare :: a -> a -> Ordering
LT代表小于(Less Than)EQ代表等于(Equal)GT代表大于(Greater Than) 这使你可以在实现Ord类型类时,轻松比较你定义的数据类型的不同值。