【问题标题】:Type constraints for automatic function constraint deduction in HaskellHaskell中自动函数约束推导的类型约束
【发布时间】:2013-05-31 10:22:09
【问题描述】:

出于教育目的,我在 Haskell 中玩树。我有这样定义的Tree a 类型

data Tree a = EmptyTree | Node a (Tree a) (Tree a)

还有许多共享基本约束的函数 - Ord a - 所以它们有类似的类型

treeInsert :: Ord a => a -> Tree a -> Tree a
treeMake :: Ord a => [a] -> Tree a

等等。我也可以这样定义Tree a

data Ord a => Tree a = EmptyTree | Node a (Tree a) (Tree a)

但我不能简化我的功能并省略额外的Ord a 如下:

treeInsert :: a -> Tree a -> Tree a
treeMake :: [a] -> Tree a

为什么 Haskell(以-XDatatypeContexts 运行)不会自动推断出这个约束?对我来说,这似乎很明显。为什么我错了?

这是一些示例源代码

data (Eq a, Ord a) => Tree a = EmptyTree | Node a (Tree a) (Tree a)

treeInsert :: a -> Tree a -> Tree a
treeInsert a EmptyTree = Node a EmptyTree EmptyTree
treeInsert a node@(Node v left right)
 | a == v = node
 | a > v = Node v left (treeInsert a right)
 | a < v = Node v (treeInsert a left) right

mkTree :: [a] -> Tree a
mkTree [] = EmptyTree
mkTree (x:xs) = treeInsert x (mkTree xs)

我收到了

Tree.hs:5:26:
    No instance for (Ord a)
      arising from a use of `Node'
    In the expression: Node a EmptyTree EmptyTree
    In an equation for `treeInsert':
        treeInsert a EmptyTree = Node a EmptyTree EmptyTree
Failed, modules loaded: none. 

【问题讨论】:

标签: haskell types type-deduction


【解决方案1】:

这是一个众所周知的关于数据声明上下文的问题。如果您定义data Ord a =&gt; Tree a = ...,它所做的只是强制任何提及Tree a 的函数具有Ord a 上下文。这不会为您节省任何打字时间,但从好的方面来说,明确的上下文清楚地说明了需要什么。

GADT 助你一臂之力!

解决方法是使用Generalised Abstract Data Types (GADTs)。

{-# Language GADTs, GADTSyntax #-}

我们可以通过提供显式类型签名将上下文直接放在构造函数上:

data Tree a where
   EmptyTree :: (Ord a,Eq a) => Tree a
   Node :: (Ord a,Eq a) => a -> Tree a -> Tree a -> Tree a

然后每当我们与Node a left right 进行模式匹配时,我们都会得到一个隐含的(Ord a,Eq a) 上下文,就像你想要的那样,所以我们可以像这样开始定义treeInsert

treeInsert :: a -> Tree a -> Tree a
treeInsert a EmptyTree = Node a EmptyTree EmptyTree
treeInsert a (Node top left right) 
          | a < top   = Node top (treeInsert a left) right
          | otherwise = Node top left (treeInsert a right) 

派生的东西

如果你只是在那里添加deriving Show,你会得到一个错误:

Can't make a derived instance of `Show (Tree a)':
  Constructor `EmptyTree' must have a Haskell-98 type
  Constructor `Node' must have a Haskell-98 type
  Possible fix: use a standalone deriving declaration instead
In the data type declaration for `Tree'

这很痛苦,但就像它说的那样,如果我们添加 StandaloneDeriving 扩展名 ({-# Language GADTs, GADTSyntax, StandaloneDeriving #-}),我们就可以执行类似的操作

deriving instance Show a => Show (Tree a)
deriving instance Eq (Tree a) -- wow

一切正常。令人惊叹的是,隐式 Eq a 上下文意味着我们不需要实例上的显式 Eq a

上下文仅来自构造函数

请记住,您只能通过使用其中一个构造函数获得隐式上下文。 (请记住,这是定义上下文的地方。)

这实际上就是为什么我们需要EmptyTree 构造函数的上下文。如果我们只是输入EmptyTree::Tree a,那一行

treeInsert a EmptyTree = Node a EmptyTree EmptyTree

等式左侧不会有(Ord a,Eq a) 上下文,因此右侧的实例将丢失,而Node 构造函数需要这些实例。这将是一个错误,因此在这种情况下保持上下文一致会很有帮助。

这也意味着你不能拥有

treeMake :: [a] -> Tree a

treeMake xs = foldr treeInsert EmptyTree xs

您将收到 no instance for (Ord a) 错误,因为左侧没有构造函数可以为您提供 (Ord a,Eq a) 上下文

这意味着你仍然需要

treeMake :: Ord a => [a] -> Tree a

这一次没有办法绕过它,抱歉,但从好的方面来说,这很可能是您想要编写的唯一一个没有树参数的树函数。 大多数树函数会在定义的左侧取一棵树并对它做一些事情,所以大部分时间你都会有隐式上下文。

【讨论】:

  • @Satvik 它不适用于mkTree,因为左侧没有Tree a 构造函数,因此您无法获得仅来自构造函数的隐式上下文。不过,这是一个很好的观点,所以我会编辑一些关于它的内容,谢谢。
  • @Satvik Done - 最后一部分解释清楚了吗?
  • 是的。它解释得很好。
  • 绝对完美且令人筋疲力尽的答案。谢谢!
  • @tomas789:详尽的答案确实常常让人筋疲力尽。
【解决方案2】:

kirelagin 关于DatatypeContexts 没用是正确的。您仍然必须在所有函数中编写类约束。但是,如果您周围有很多课程,那么这里有一个小技巧,这样您就可以逃脱只有一个课程。

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}

data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving Show

class (Eq a, Ord a) => Foo a where

instance (Eq a, Ord a) => Foo a where

treeInsert :: Foo a => a -> Tree a -> Tree a
treeInsert a EmptyTree = Node a EmptyTree EmptyTree
treeInsert a node@(Node v left right)
 | a == v = node
 | a > v = Node v left (treeInsert a right)
 | a < v = Node v (treeInsert a left) right

mkTree :: Foo a => [a] -> Tree a
mkTree [] = EmptyTree
mkTree (x:xs) = treeInsert x (mkTree xs)

现在Foo 类就像Eq &amp;&amp; Ord。使用类似的示例,您可以在所有函数中只用一个类替换所有类。正如@luqui 所指出的,您也可以使用ConstraintKinds 使其工作。

或者您可以使用 GADT,我认为它可以让您在数据定义中提及类约束。

【讨论】:

  • 或者,ConstraintKinds,只是type Foo a = (Eq a, Ord a)
  • @AndrewC 感谢您指出这一点。问题是你不能完全避免所有函数中的上下文(mkTree 的例子)。已经有一个答案指出@raymond 关于如何使用GADTs 来实现接近的目标。当有许多类在所有函数中重复时,我只想指出一种简化上下文的方法。
  • Errm EqOrd 的超类。如果你说:: Ord a =&gt; ...,你将免费获得Eq a
【解决方案3】:

嗯,问题是这个约束适用于构造函数,而不是整个数据类型。这就是为什么DatatypeContexts 实际上几乎没用……你可以阅读更多关于它的信息here

如果您希望第二段包含解决方案,那么您很不幸,很不幸。我不知道这样的解决方案,而且似乎确实是does not exist。 wiki 文章提到了 MultiParamTypeClasses 的用法,但老实说,这并不方便。

【讨论】:

  • 我已经更新了那个维基页面,比如I suggested some time ago
  • 我完全同意多参数类型类的“不那么方便,老实说”。
猜你喜欢
  • 2013-08-19
  • 1970-01-01
  • 2012-03-21
  • 2015-09-06
  • 1970-01-01
  • 2017-04-11
  • 1970-01-01
  • 2013-11-05
相关资源
最近更新 更多