平等的阶级误解

时间:2012-11-22 04:53:53

标签: haskell typeclass

  • 我有自己的数据类型来表示图形的节点和边缘,如下所示:

    data Node a = Node a deriving (Show, Eq)
    
    data Label a = Label a deriving (Show)
    
    data Cost = CostI Int | CostF Float deriving (Show)
    
    data Edge label node = Edge (Label label, (Node node,Node node), Cost) deriving (Show)
    
  • 现在,我创建一个函数来检查边是否包含2个节点,如下所示:

    isEdge:: (Eq n) => (Edge l n) -> (Node n, Node n) -> Bool
    isEdge (Edge (_, (n1,n2), _)) (n3, n4) = result
         where result = (n1 == n3) && (n2 == n4)
    
  • 该功能效果很好,这里的问题是如果我从函数中删除(Eq n),它就会失败。那么,为什么会这样,即使在上面的声明中我声明Node是从Eq类派生出来的?

    data Node a = Node a deriving (Show, Eq)
    

2 个答案:

答案 0 :(得分:8)

Eq导出的Node a实例GHC是这样的:

instance Eq a => Eq (Node a) where
    (Node x) == (Node y) = x == y
    (Node x) /= (Node y) = x /= y

您可以通过使用-ddump-deriv进行编译来查看生成的代码。出于显而易见的原因,需要Eq a约束。因此,GHC无法推断Eq的实例,例如Node (a -> b),因为无法比较函数。

然而,GHC无法推断Eq Node a的实例某些 a并不意味着它会阻止您构造Node a类型的值,其中a不是相等类型。


如果您想阻止人们构建不可比较的Node,您可以尝试设置这样的约束:

data Eq a => Node a = Node a deriving (Eq, Show)

但现在GHC告诉我们我们需要一个编译器编译指示:

Illegal datatype context (use -XDatatypeContexts): Eq a =>

好的,我们将它添加到我们文件的顶部:

{-# LANGUAGE DatatypeContexts #-}

现在编译:

/tmp/foo.hs:1:41: Warning: -XDatatypeContexts is deprecated: It was widely
considered a misfeature, and has been removed from the Haskell language.

问题是现在使用Node的每个函数都需要一个Eq类约束,这很烦人(你的函数仍然需要约束!)。 (另外,如果您的用户想要使用非相等类型创建Node但是从不测试它们是否相等,那么问题是什么?)


实际上有一种方法可以让GHC做你想做的事情:广义代数数据类型(GADTs):

{-# LANGUAGE GADTs, StandaloneDeriving #-}

data Node a where
  Node :: Eq a => a -> Node a

这看起来就像你的原始定义,除了它强调Node 值构造函数(以前在数据声明右侧的那个)只是一个函数,你可以添加约束。现在GHC知道只有相等类型可以放入Node,并且与我们之前尝试的解决方案不同,我们可以创建不需要约束的新函数:

fromNode :: Node a -> a
fromNode (Node x) = x

我们仍然可以派生EqShow个实例,但语法略有不同:

deriving instance Eq   (Node a)
deriving instance Show (Node a)

(因此上面是StandaloneDeriving pragma。)

为了实现这一点,GHC还要求我们向GADT添加Show约束(如果再次查看生成的代码,您将看到约束现已消失):

data Node a where
  Node :: (Eq a, Show a) => a -> Node a

现在我们可以将Eq约束关闭isEdge,因为GHC可以推断它!

(这对于这样一个简单的情况来说肯定是有点过分了 - 再次,如果人们想构建其中包含函数的节点,为什么不应该呢?但是,当你想强制执行时,GADT在非常类似的情况下非常有用数据类型的属性。请参阅a cool example)。


编辑(从未来):你也可以写

data Node a = (Eq a, Show a) => Node a

但您仍需要启用GADT扩展并单独派生实例。请参阅this thread

答案 1 :(得分:5)

当您向数据声明添加deriving子句时,derived子句将在声明的范围内包含类型变量的任何必要约束。在这种情况下,deriving Eq将基本上创建以下实例:

instance Eq a => Eq (Node a) where
    (Node a) == (Node b) = a == b
    (Node a) /= (Node b) = a /= b

任何派生的Eq实例都将取决于数据构造函数右侧显示的类型的Eq实例。

这是因为实际上没有其他方法可以自动派生Eq实例。如果它们具有相同的类型并且它们的所有组件相等,则两个值相等。因此,您需要能够测试组件是否相等。为了通常测试多态组件的相等性,您需要一个Eq实例。

这不仅适用于Eq,而且适用于所有派生类。例如这段代码

toStr :: Edge l n -> String
toStr = show
如果不添加约束(Show l, Show n)

将无效。没有该约束,显示Edge的函数不知道要调用什么来显示其内部标签和节点。