在Haskell中实现类型类时的任意类约束

时间:2014-08-21 09:06:50

标签: haskell

我正在尝试在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

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

然而,如果BinarySearchTree需要一些与Eq完全不相容的异国类别约束,该怎么办?我如何表达这些并让类型检查员满意?

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

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

不幸的是,这似乎不满足类型检查器:即使声明保证BinarySearchTree 必须包含具有Ord实例的元素,但此约束不会似乎延续到使用BinarySearchTree的函数 - 就好像类约束只应用于数据构造函数,但后来被遗忘了。

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

2 个答案:

答案 0 :(得分:9)

你在询问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,它更详细地解释了这些想法。

答案 1 :(得分:0)

我有一些似乎有用的东西。

需要重新定义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方法,你需要显式地输入它的返回值。只要您打算这样做,您也可以使用空构造函数进行底层实现。