Return 来自函数的 GADT

Return GADT from function

背景

我正在 Haskell 中编写一个红黑树实现,使用 依赖类型并且在理解为什么下面的代码不起作用时遇到了一些麻烦。作为一种热身练习,我想做的是找到一个给定任意值的子树。不幸的是,我在编译代码时遇到了一些麻烦,最终继续前进。但是,我仍然对为什么这不能编译以及我可以做些什么来让它工作感到困惑。我找不到任何这样的例子,也找不到任何说明为什么这不起作用的东西。

代码

data NaturalNum = Z
                | S NaturalNum
                deriving Show


data Color :: * where
  Red :: Color
  Black :: Color
  deriving Show


data Tree :: Color -> NaturalNum -> * -> * where
  Empty :: Tree Black Z a
  RTree :: Tree Black natNum a     -> a -> Tree Black natNum a      -> Tree Red natNum a
  BTree :: Tree leftColor natNum a -> a -> Tree rightColor natNum a -> Tree Black (S natNum) a
deriving instance (Show a) => Show (Tree c n a)


findSubtree :: (Ord a) => Tree c n a -> a -> Tree cc nn a
findSubtree Empty _ = Empty
findSubtree (RTree left curr right) item
  | curr == item = RTree left curr right
  | curr < item  = findSubtree left item
  | otherwise    = findSubtree right item
findSubtree (BTree left curr right) item
  | curr == item = BTree left curr right
  | curr < item  = findSubtree left item
  | otherwise    = findSubtree right item

错误信息

还有两个几乎相同的错误消息抱怨其他构造函数。

• Could not deduce: cc ~ 'Black
  from the context: (c ~ 'Black, n ~ 'Z)
    bound by a pattern with constructor:
               Empty :: forall a. Tree 'Black 'Z a,
             in an equation for ‘findSubtree’
    at lib/RedBlackTree.hs:246:13-17
  ‘cc’ is a rigid type variable bound by
    the type signature for:
      findSubtree :: forall (c :: Color) (n :: NaturalNum) a (cc :: Color) (nn :: NaturalNum).
                     Tree c n a -> a -> Tree cc nn a
    at lib/RedBlackTree.hs:245:16
  Expected type: Tree cc nn a
    Actual type: Tree 'Black 'Z a
• In the expression: Empty
  In an equation for ‘findSubtree’: findSubtree Empty _ = Empty
• Relevant bindings include
    findSubtree :: Tree c n a -> a -> Tree cc nn a
      (bound at lib/RedBlackTree.hs:246:1)

我的困惑

根据错误消息,我认为我无法 return 由不受约束的颜色和黑色高度参数化的 GADT。然而,我觉得这相当不令人满意。我发现如果你对 findSubtree 函数的 return 类型有这么少的信息,就很难检查程序其他部分的类型,但是找到一个子树似乎是一件相当合理的事情想做。这是使用 GADT 的限制吗?

最重要的是, 我必须对上述代码进行哪些更改才能从该数据结构中 return 任意子树,以及为什么给定代码无法编译的确切解释是什么?

问题出在 findSubtree 的类型签名中的量词。

findSubtree :: (Ord a) => Tree c n a -> a -> Tree cc nn a

虽然您显然希望 cna 被普遍量化,但并不是说 ccnn 应该被存在量化(找到的子树将具有 一些 颜色)。

在Haskell中处理存在性的方法是将它们包装在数据类型中。这不是很明显,但是在数据类型中有一个 forall 等同于一个 exists(如果它存在于 Haskell)。

-- An existential type to represent a tree with _some_ color and depth
data SomeTree a where
  SomeTree :: Tree c n a -> SomeTree a

findSubtree :: (Ord a) => Tree c n a -> a -> SomeTree a
findSubtree Empty _ = SomeTree Empty
findSubtree (RTree left curr right) item
  | curr == item = SomeTree (RTree left curr right)
  | curr < item  = findSubtree left item
  | otherwise    = findSubtree right item
findSubtree (BTree left curr right) item
  | curr == item = SomeTree (BTree left curr right)
  | curr < item  = findSubtree left item
  | otherwise    = findSubtree right item

问题在这里:

findSubtree :: (Ord a) => Tree c n a -> a -> Tree cc nn a

类型变量 a,c,n,cc,nn 是通用量化的,这意味着 调用者 可以为这些变量选择他们想要的任何 type/color。显然,调用者必须能够选择 a,c,n,但在代码中选择 cc,nn 的是函数本身。

编译器抱怨结果与调用者的选择不匹配,就像

foo :: Int -> a
foo x = x -- error: what if the caller chooses e.g. a ~ Bool ?

一个可能的解决方案是将结果包装在一个存在的包装器中,如@Alec 所示。