在定义类型类时,你如何决定在类型类中包含/排除一个函数'定义?例如,这两种情况之间有什么区别:
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 = ...
答案 0 :(得分:10)
我认为这里有一些紧张的因素。一般认为类型类定义应该最小,并且只包含独立函数。正如bhelkir的回答所解释的那样,如果您的班级支持a
,b
和c
,c
可以a
和b
,这是在课堂外定义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
,但fmap
,pure
和join
也足以实施>>=
}}
与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 a
和f b
值,但对于Applicative
类的大多数用途,我们实际上并不想要那些元组,我们只想组合从f a
和f b
中提取的值;规范定义允许我们选择用什么函数来组合。
另一个性能考虑因素是,即使某个类中的某些方法可以根据其他方法定义,这些通用定义对于所有类的实例也可能不是最佳的。如果我们以Foldable
为例,foldMap
和foldr
是可以确定的,但某些类型比另一种更有效。因此,我们经常使用非最小类定义来允许实例提供方法的优化实现。
答案 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
中的一个或两个,否则您将拥有无限循环。但是,这些是重载的有用函数,因此它们都保留在类型类中。