我有自己的数据类型来表示图形的节点和边缘,如下所示:
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)
答案 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
我们仍然可以派生Eq
和Show
个实例,但语法略有不同:
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
的函数不知道要调用什么来显示其内部标签和节点。