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

    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)
    
4

2 回答 2

8

EqGHC 派生的实例Node a是这样的:

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

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

然而,GHC 无法推断 for some 的实例这一事实Eq并不意味着Node a它会阻止您构造一个不是相等类型的类型值。 aNode aa


如果您想阻止人们构建 non-comparable Nodes,您可以尝试设置这样的约束:

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.

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


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

{-# LANGUAGE GADTs, StandaloneDeriving #-}

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

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

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

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

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

(因此上面的 StandaloneDeriving 杂注。)

为此,GHC 还要求我们向ShowGADT 添加一个约束(如果您再次查看生成的代码,您会发现约束现在消失了):

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

现在我们可以取消Eq约束isEdge,因为 GHC 可以推断它!

(对于这样一个简单的情况,这绝对是矫枉过正——同样,如果人们想在其中构建带有函数的节点,为什么不呢?但是,当你想强制执行你的某些属性时,GADT 在非常相似的情况下非常有用数据类型。看一个很酷的例子)。


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

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

但是您仍然需要单独启用 GADT 扩展和派生实例。看到这个线程

于 2012-11-22T05:53:48.633 回答
5

当您将deriving子句添加到数据声明时,派生子句将包含声明范围内类型变量的任何必要约束。在这种情况下,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不知道调用什么来显示它的内部标签和节点。

于 2012-11-22T05:21:19.523 回答