类型类:具有默认实现的功能与单独的功能

时间:2015-03-10 19:37:47

标签: haskell typeclass

在定义类型类时,你如何决定在类型类中包含/排除一个函数'定义?例如,这两种情况之间有什么区别:

class Graph g where
    ...

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

vs

class Graph g where
    ...

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

2 个答案:

答案 0 :(得分:10)

我认为这里有一些紧张的因素。一般认为类型类定义应该最小,并且只包含独立函数。正如bhelkir的回答所解释的那样,如果您的班级支持abcc可以ab,这是在课堂外定义c的论据。

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

首先,通常有多个最小的操作集可以等效地定义同一个类。 Haskell中Monad的经典定义就是这个(清理了一下):

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

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

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

答案 1 :(得分:8)

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

将一个函数保存在类型类之外意味着它不能被修改,而且它不会使类混乱。以mapM函数为例。没有必要将它放在Monad类中,并且您可能不希望人们编写自己的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类型类的一部分,它只是一个碰巧使用Num方法的随机函数。它与Num的含义无关。

真的,它归结为设计。函数通常在类型类中指定,如果它们与作为该类型类的实例的含义不一致。有时函数包含在类型类中,但具有默认定义,因此特定类型可以使其超载以使其更有效,但在大多数情况下,将类成员保持在最低限度是有意义的。查看它的一种方法是提出问题“这个函数是否只能用类的约束来实现?”如果答案是否定的,则应该在课堂上。如果答案是肯定的,那么绝大多数时候它意味着该函数应该移到该类之外。只有当能够超载它所获得的价值时才应将其移入课堂。如果重载该函数可以破坏其他期望它以特定方式运行的代码,那么它不应该被重载。

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

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中的一个或两个,否则您将拥有无限循环。但是,这些是重载的有用函数,因此它们都保留在类型类中。