类型类:具有默认实现的函数与单独的函数

Typeclasses: function with default implementation vs separate function

定义类型类时,您如何决定 including/excluding 类型类定义中的函数?比如这2种情况有什么区别:

class Graph g where
    ...

    insertNode :: g -> Node -> g
    insertNode graph node = ...

对比

class Graph g where
    ...

insertNode :: (Graph g) => g -> Node -> g
insertNode graph node = ...

在类型定义中包含一个函数class 意味着它可以被覆盖。在这种情况下,您需要它位于 Graph 类型 class 内,因为它 returns 一个 Graph g => g,并且 Graph 的每个特定实例都需要知道如何构建该值。或者,您可以在类型 class 中指定一个函数以构造类型 Graph g => g 的值,然后 insertNode 可以在其结果中使用该函数。

将函数保留在类型之外class 意味着它不能被修改,但也不会弄乱 class。以 mapM 函数为例。没有必要在 Monad class 中,您可能不希望人们编写自己的 mapM 实现,它应该在所有上下文中做同样的事情。作为另一个例子,考虑函数

-- f(x) = 1 + 3x^2 - 5x^3 + 10x^4
aPoly :: Num a => a -> a
aPoly x = 1 + 3 * x * x - 5 * x * x * x + 10 * x * x * x * x

显然aPoly不应该是Num类型class的一部分,它只是一个随机函数,恰好使用了Num方法。它与 Num.

的含义无关

真的,归结为设计。函数通常在类型class 中指定,如果它们是该类型class 实例的组成部分。有时函数包含在类型 class 中但具有默认定义,以便特定类型可以重载它以使其更高效,但在大多数情况下,将 class 成员保持在最低限度是有意义的.一种看待它的方法是问问题 "Can this function be implemented with only the constraint of the class?" 如果答案是否定的,它应该在 class 中。如果答案是肯定的,绝大多数情况下这意味着该函数应该移到 class 之外。只有当能够重载获得价值时,才应将其移至 class。如果重载该函数会破坏其他期望它以特定方式运行的代码,则不应重载它。

要考虑的另一种情况是,当您的类型class 中的函数具有合理的默认值,但这些默认值是相互依赖的。以Num class为例,你有

class Num a where
    (+) :: a -> a -> a
    (*) :: a -> a -> a
    (-) :: a -> a -> a
    a - b = a + negate b
    negate :: a -> a
    negate a = 0 - a
    abs :: a -> a
    signum :: a -> a
    fromInteger :: Integer -> a

请注意,(-)negate 都是根据彼此实现的。如果您创建自己的数字类型,那么您将需要实现 (-)negate 中的一个或两个,否则您将陷入无限循环。不过,这些都是有用的重载函数,因此它们都位于类型 class.

我认为这里有一些紧张因素。一般认为类型 class 定义应该是 最小 ,并且只包含 independent 函数。正如 bhelkir 的回答所解释的那样,如果您的 class 支持函数 abc,但是 c 可以根据 ab,这是在 class.

之外定义 c 的参数

但是这个总体想法遇到了其他一些相互矛盾的问题。

首先,通常有不止一组最小的操作可以等效地定义相同的 class。 Haskell 中 Monad 的 classic 定义是这样的(清理了一下):

class Monad m where
    return :: a -> m a
    (>>=) :: m a -> (a -> m b) -> m b

但众所周知还有其他定义,例如:

class Applicative m => Monad m where
    join :: m (m a) -> m a

return>>=足以实现join,但fmappurejoin也足以实现>>=.

Applicative 类似。这是规范的 Haskell 定义:

class Functor f => Applicative f where
    pure  :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

但以下任何一项都是等价的:

class Functor f => Applicative f where
    unit  :: f ()
    (<*>) :: f (a -> b) -> f a -> f b

class Functor f => Applicative f where
    pure  :: a -> f a
    fpair :: f a -> f b -> f (a, b)

class Functor f => Applicative f where
    unit  :: f ()
    fpair :: f a -> f b -> f (a, b)

class Functor f => Applicative f where
    unit  :: f ()
    liftA2 :: (a -> b -> c) -> f a -> f b -> f c

给定任何这些 class 定义,您可以将任何其他方法中的任何方法编写为 class 之外的派生函数。为什么选第一个?我无法权威地回答,但我认为它把我们带到了第三点:性能考虑。其中许多 fpair 操作通过创建元组组合 f af b 值,但对于 Applicative class 的大多数用途,我们实际上并不想要那些元组,我们只想组合从 f af b 中提取的值;规范定义允许我们选择用什么函数来做这个组合。

另一个性能考虑因素是,即使 class 中的某些方法可以根据其他方法进行定义,这些通用定义对于 class 的所有实例也可能不是最佳的。如果我们以 Foldable 为例,foldMapfoldr 是可以相互定义的,但有些类型比另一种更有效地支持一种。因此,我们经常使用非最小 class 定义来允许实例提供优化的方法实现。