跳转至内容

Haskell/高级类型类

来自 Wikibooks,开放世界中的开放书籍

类型类可能看起来无害,但对该主题的研究已经导致了一些进步和概括,这些进步和概括使它们成为一个非常强大的工具。

多参数类型类

[编辑 | 编辑源代码]

多参数类型类是对单参数类型类的泛化,并由一些 Haskell 实现支持。

假设我们想创建一个“集合”类型类,它可以与各种具体的數據類型一起使用,并支持两个操作——“插入”用于添加元素,以及“成员”用于测试成员资格。第一次尝试可能看起来像这样

示例:Collection 类型类(错误)

 class Collection c where
     insert :: c -> e -> c
     member :: c -> e -> Bool

 -- Make lists an instance of Collection:
 instance Collection [a] where
     -- insert :: [a] -> e -> [a]
     insert xs x = x:xs
     -- member :: [a] -> e -> Bool
     member = flip elem

然而,这将无法编译,因为 (:) 的参数必须是 [a]a 类型,而我们提供的是 e。问题是 Collection 操作中的 'e' 类型变量来自无处——Collection 实例的类型中没有任何内容可以告诉我们 'e' 到底是什么,因此我们永远无法定义这些方法的实现。多参数类型类通过允许我们将 'e' 放入类的类型来解决这个问题。这是一个可以编译并使用的示例

示例:Collection 类型类(正确)

 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 class Eq e => Collection c e where
     insert :: c -> e -> c
     member :: c -> e -> Bool

 instance Eq a => Collection [a] a where
     insert = flip (:)
     member = flip elem

函数依赖

[编辑 | 编辑源代码]

上面示例中的一个问题是,在这种情况下,我们拥有编译器不知道的额外信息,这会导致错误的歧义和过于泛化的函数签名。在这种情况下,我们可以直观地看到集合的类型将始终决定它包含的元素的类型——所以如果 c[a],那么 e 将是 a。如果 cHashmap a,那么 e 将是 a。(反之则不然:许多不同的集合类型可以保存相同的元素类型,因此知道元素类型是例如 Int,不会告诉你集合类型)。

为了告诉编译器这些信息,我们添加一个函数依赖,将类声明更改为

示例:函数依赖

 {-# LANGUAGE FunctionalDependencies #-}
 class Eq e => Collection c e | c -> e where ...

函数依赖是我们可以对类型类参数施加的约束。这里,额外的 | c -> e 应该读作“c 唯一标识 e”,意味着对于给定的 c,将只有一个 e。你可以在一个类中拥有多个函数依赖——例如,在上述情况下,你可以拥有 c -> e, e -> c。并且你可以在多参数类中拥有两个以上的参数。

矩阵和向量

[编辑 | 编辑源代码]

假设你想实现一些代码来执行简单的线性代数

示例:VectorMatrix 数据类型

 data Vector = Vector Int Int deriving (Eq, Show)
 data Matrix = Matrix Vector Vector deriving (Eq, Show)

你希望它们的行为尽可能像数字。因此,你可能会从重载 Haskell 的 Num 类开始

示例:VectorMatrix 的实例声明

instance Num Vector where
  Vector a1 b1 + Vector a2 b2 = Vector (a1+a2) (b1+b2)
  Vector a1 b1 - Vector a2 b2 = Vector (a1-a2) (b1-b2)
  {- ... and so on ... -}

instance Num Matrix where
  Matrix a1 b1 + Matrix a2 b2 = Matrix (a1+a2) (b1+b2)
  Matrix a1 b1 - Matrix a2 b2 = Matrix (a1-a2) (b1-b2)
  {- ... and so on ... -}

问题出现在你想要开始将数量相乘时。你真的需要一个重载到不同类型的乘法函数

示例:我们需要什么

(*) :: Matrix -> Matrix -> Matrix
(*) :: Matrix -> Vector -> Vector
(*) :: Matrix -> Int -> Matrix
(*) :: Int -> Matrix -> Matrix
{- ... and so on ... -}

我们如何指定一个类型类来允许所有这些可能性?

我们可以尝试这个

示例:无效的尝试(过于泛化)

class Mult a b c where
  (*) :: a -> b -> c

instance Mult Matrix Matrix Matrix where
  {- ... -}

instance Mult Matrix Vector Vector where
  {- ... -}

然而,那并不是我们真正想要的。就目前而言,即使是像这样的简单表达式也具有模糊的类型,除非你在中间表达式上提供额外的类型声明

示例:歧义导致代码更冗长

m1, m2, m3 :: Matrix
(m1 * m2) * m3              -- type error; type of (m1*m2) is ambiguous
(m1 * m2) :: Matrix * m3    -- this is ok

毕竟,没有任何东西能阻止别人后来添加另一个实例

示例:Mult 的无意义实例

instance Mult Matrix Matrix (Maybe Char) where
  {- whatever -}

问题是 c 不应该是一个自由类型变量。当你了解你正在相乘的事物的类型时,结果类型应该仅从此信息中确定。

你可以通过指定函数依赖来表达这一点

示例:Mult 的正确定义

class Mult a b c | a b -> c where
  (*) :: a -> b -> c

这告诉 Haskell cab 唯一确定。


华夏公益教科书