【问题标题】:Arbitrary class constraints when implementing type classes in Haskell在 Haskell 中实现类型类时的任意类约束
【发布时间】:2014-08-26 15:23:14
【问题描述】:

我正在尝试在 Haskell 中实现一个简单的 Set,但我一直不知道如何为它包含的元素表达类约束。

Set 类型类相当简单:

class Set s where
  empty    :: s a
  isEmpty  :: s a -> Bool
  insert   :: s a -> a -> s a
  contains :: s a -> a -> Bool

Set 的一个简单实现是二叉搜索树:

data BinarySearchTree a = Node a (BinarySearchTree a) (BinarySearchTree a) | Leaf

然而,在声明正确的类约束时,我有点卡住了:

  • Set 至少需要能够确定两个元素是否相等 - 它的值需要有一个 Eq 的实例。
  • BinarySearchTree 要求其元素具有 Ord 的实例,因为每个节点都需要在左侧具有较小的元素,在右侧具有较大的元素。

第一个简单的解决方案是更新Set 的签名以要求a 拥有Eq 的实例:

class Set s where
  empty    :: Eq a => s a
  isEmpty  :: Eq a => s a -> Bool
  insert   :: Eq a => s a -> a -> s a
  contains :: Eq a => s a -> a -> Bool

BinarySearchTree 实现Set 的实例不是问题:Ord 隐含Eq,因此我可以“覆盖”类约束。

但是,如果BinarySearchTree 需要一些与Eq 完全不兼容的奇异类约束怎么办?我该如何表达这些并让类型检查器满意?

我能想到的唯一方法是在BinarySearchTree 上添加一个类约束,例如:

data Ord a => BinarySearchTree a = Node a (BinarySearchTree a) (BinarySearchTree a) | Leaf

不幸的是,这似乎不能满足类型检查器的要求:即使声明保证 BinarySearchTree 必须 包含带有 Ord 实例的元素,这个约束似乎并没有延续对于使用 BinarySearchTree 的函数 - 就好像类约束仅适用于数据构造函数,但随后被遗忘了。

我错过了什么?对于我正在尝试做的事情,是否有一个优雅的解决方案,甚至是一个解决方案?

【问题讨论】:

  • 我的观点似乎有一点不正确:我确定我已经设法完成了整个“Eq on Set, Ord on @987654349 @" 的东西,但我不能再编译了....
  • 注意:这个问题与另一个问题有关:stackoverflow.com/questions/25345055/…。不确定它是否足够接近以被视为重复...
  • 我不确定它们是否符合重复的条件,尽管它们似乎都来自同一个根本问题。无论哪种方式对我来说都很好,我不会因为我的问题作为重复而被冒犯。

标签: haskell


【解决方案1】:

您问的是 Haskell 中的一个众所周知的问题:如何定义类型类,以便类型类的实例可以定义类型类操作所需的类型类约束。这个问题经常以“为什么Data.Set 不是Functor 的实例?”的形式出现在论坛上。然后问题是Data.Set 有一个带有额外Ord 约束的map 函数:

Data.Set.map :: Ord b => (a -> b) -> Set a -> Set b 

而 Functor 的 fmap 方法看起来像

class Functor f where
  fmap :: (a -> b) -> f a -> f b

几年来,问题的解决方案已经存在。一种解决方案将相对较新的扩展 ConstraintKinds 与 TypeFamilies 相结合。对于您的示例,它相当于:

class Set s where
  type SetCt s a :: Constraint
  empty    :: s a
  isEmpty  :: s a -> Bool
  insert   :: Ct a => s a -> a -> s a
  contains :: Ct a => s a -> a -> Bool

BinarySearchTree 的实例如下所示

instance Set BinarySearchTree where
  type SetCt BinarySearchTree a = Ord a
  ...

这是 Dominic Orchard 的blog post,它更详细地解释了这些想法。

【讨论】:

  • 在我非常具体的情况下,似乎我可以通过不对Set 设置任何约束并将Ord a 约束添加到BinarySearchTree 的数据构造函数来解决ExistentialQuantification 的问题。有什么特别的理由不这样做吗?
  • 我不确定我理解你的意思。您是否保留原始问题中显示的 Set 类?如果是这样,是什么阻止我为没有 Ord a 实例的 a 类型的 xy 调用 insert (insert empty x) y
  • 抱歉,不是很清楚。我已经回答了我自己的问题以提供更多详细信息,并希望您能提供任何见解 - 我已经是一个 Haskell 初学者了,很有可能我没有看到我提出的实施的所有后果。
【解决方案2】:

我有一些似乎可以工作的东西。

需要重新定义Set如下:

class Set s where
  isEmpty  :: s a -> Bool
  insert   :: s a -> a -> s a
  contains :: s a -> a -> Bool

注意与之前定义的区别:

  • 没有类限制。
  • 没有empty 方法。

以这种方式定义了Set,然后我可以将BinarySearchTree定义为:

{-# LANGUAGE ExistentialQuantification #-}
data BinarySearchTree a = Ord a => Node a (BinarySearchTree a) (BinarySearchTree a)
                        | Ord a => Leaf

这是可行的,因为 BinarySearchTree 不能在没有隐式 Ord 的情况下被构造 - 每当你得到一个实例 BinarySearchTree a 时,你也有 Ord a

请注意,这需要更改 Set 签名以不再包含 empty 定义:因为它没有收到 BinarySearchTree a 的实例但必须创建一个实例,所以无法强制执行 Ord a 约束.

在这个特定的例子中,我认为这不是什么大问题:除非我弄错了,否则为了使用empty 方法,您需要显式键入它的返回值。只要你打算这样做,你还不如将空构造函数用于底层实现。

【讨论】:

  • 当然,如果您不需要重载的empty,那么这可行。这确实意味着任何确实需要empty 的函数都必然会被限制为单个Set 实现。根据您的用例,这可能不是问题,但它确实限制了您的解决方案的通用性。
猜你喜欢
  • 1970-01-01
  • 2015-09-06
  • 1970-01-01
  • 2013-11-05
  • 1970-01-01
  • 2012-03-21
  • 2021-03-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多