我有自己的数据类型来表示图的节点和边,如下所示:
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 回答
Eq
GHC 派生的实例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
它会阻止您构造一个不是相等类型的类型值。 a
Node a
a
如果您想阻止人们构建 non-comparable Node
s,您可以尝试设置这样的约束:
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
s 的函数都需要一个Eq
类约束,这很烦人(你的函数仍然需要约束!)。(另外,如果您的用户想要Node
使用非相等类型创建 s 但从不测试它们的相等性,那么问题是什么?)
然而,实际上有一种方法可以让 GHC 做你想做的事:广义代数数据类型 ( GADTs ):
{-# LANGUAGE GADTs, StandaloneDeriving #-}
data Node a where
Node :: Eq a => a -> Node a
这看起来就像您的原始定义,除了它强调Node
值构造函数(以前在数据声明的右侧)只是一个函数,您可以向其中添加约束。现在 GHC 知道只能将相等类型放入Node
s 中,并且与我们之前尝试的解决方案不同,我们可以创建不需要约束的新函数:
fromNode :: Node a -> a
fromNode (Node x) = x
我们仍然可以派生Eq
和Show
实例,但语法略有不同:
deriving instance Eq (Node a)
deriving instance Show (Node a)
(因此上面的 StandaloneDeriving 杂注。)
为此,GHC 还要求我们向Show
GADT 添加一个约束(如果您再次查看生成的代码,您会发现约束现在消失了):
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 扩展和派生实例。看到这个线程。
当您将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
不知道调用什么来显示它的内部标签和节点。