为什么通过存在和约束进行子类型化不起作用?

Why doesn't subtyping via existentials and constraints work?

我一直在尝试了解 Haskell 如何处理子类型,所以我想到了以下代码片段:

{-# LANGUAGE RankNTypes #-}

f1 :: () -> Int
f1 _ = 5

f2 :: () -> (forall a. Integral a => a)
f2 = f1

f2 = f1 行失败并出现意外错误消息:

Couldn't match type ‘a’ with ‘Int’
  ‘a’ is a rigid type variable bound by
    the type signature for:
      f2 :: forall a. Integral a => () -> a

好像把存在的提升为普遍的了,这当然是无意的。

我的猜测是,在实现方面 f2 需要 return 一个值和相应的字典,而 f1 只是 return 类型的值 Int.然而,从逻辑的角度来看,f2 的合同是 "a function from () to some unknown instance of Integral" 并且 f1 完美地满足它。

GHC 应该做一些隐含的魔法来让它工作还是我遗漏了什么?

你在 f2 中的 a 类型是普遍量化的,而不是存在量化的。 GHC 不直接 支持您正在寻找的那种存在类型。

但是,您可以通过将其包装在新的数据类型中来获得类似的内容:

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ConstraintKinds           #-}

data Constrained c = forall a. c a => Constrained a

f1 :: () -> Int
f1 _ = 5

f2 :: () -> Constrained Integral
f2 unit = Constrained (f1 unit)

现在,这通常不是很有用,因为我们已经完成了所有关于 f2 () 的(类型)信息,除了它的类型是 [=14= 的实例].您不再知道它是 Int。也可以跟踪此信息,但这可能有用也可能没用,具体取决于您在做什么。

关于您正在做的事情和您希望看到的内容的更多背景信息将使我更容易添加关于这些事情的什么样的信息。

附带说明:没有必要将它们变成带有参数的函数。你可以只有 f1 :: Intf2 :: Constrained Integral.

此外,如果我没有提到这一点,我会觉得有点疏忽,尽管在这个答案 Haskell 的早些时候淡化了这些类型的效用,。虽然我们有点讨论这个主题,但可能还值得指出 ConstraintKinds 是一个强大的扩展,它的用途不仅仅是受约束的存在。

你读错了 f2 的类型。

f2的类型是说“你给我一个(),我就给你一个(forall a. Integral a => a)。也就是f2承诺给你一个可以像在 任何 类型中一样使用的值是 Integral.

的成员

然而 f2 的实现表示它将通过简单地调用 f1 来实现。但是f1只有return个Int!这确实是 Integral 的成员,但它不能用作 任何 类型,它是 Integral.

的成员

事实上,类型 f1 实际上是 f2 类型的子类型,而不是相反!这确实有效:

{-# LANGUAGE RankNTypes #-}

f1 :: () -> Int
f1 = f2

f2 :: () -> (forall a. Integral a => a)
f2 _ = 5

之所以有效,是因为数字文字是多态的,因此 5 可以是任何 Num 类型(并且 NumIntegral 的超 class , 所以如果它可以是任何 Num 它也可以是任何 Integral)。 f1 然后调用 f2 请求 Int 成为 a 的选择。

您注意到 GHC 似乎正在将您写入的类型转换为 f2 :: forall a. Integral a => () -> a,您是对的;它确实实际上规范了您写入该类型的内容。原因是这两种类型实际上与 GHC 类型系统的工作方式相同。调用者实例化在 f2 的整个类型上量化的任何类型变量,调用者也将是接收 return 值的人,因此实例化仅在 return 上量化的任何类型变量价值。如果将 if 作用域覆盖整个类型或仅覆盖函数的 return 类型,则使用 forall 引入类型变量是相同的;只有在箭头的 左侧 范围内(这是更高级别的类型出现的地方),它才会有所不同。