如何使用 :~: 来确定 Haskell 中的类型相等性?

How can I use :~: to determine type equality in Haskell?

我正在尝试使用 Data.Type.Equality 中的 :~: 在编译时确定类型相等性。我的期望是它的行为符合 Scala 确定类型相等性的标准方式:

case class Equals[A >: B <:B , B]()

Equals[Int, Int]     // compiles fine
Equals[String, Int]  // doesn't compile

所以我尝试了

foo :: Int :~: Bool
foo = undefined

我预计会失败,因为 Int :~: Bool 是适合居住的。但它编译得很好。

就像我们 Haskell 用户经常假装不一样一样,每个正常的 1 输入 Haskell 都是有人居住的。这包括 data Void,并且它包括所有 aba :~: b。除了我们通常承认的礼貌价值观,还有底线价值观。

undefined :: a 是在任何类型 a 中生成底值的一种方法。因此,特别是 undefined :: Int :~: Bool,因此您的代码完全 type 正确。

如果你想要一个在编译时无法证明相等性的情况下无法编译的类型相等性,那么你需要一个类型相等性约束(即 ~ 运算符),而不是 :~:类型。你这样使用它:

foo :: Int ~ Int => ()   -- compiles fine
foo = ()

bar :: Int ~ Bool => ()  -- does technically compile
bar = ()

bar 编译只是因为在具有约束的函数体中假定 约束。但是任何 call bar 的尝试都需要编译器能够证明约束才能编译 call。所以这失败了:

baz :: ()
baz = bar
然而,

:~: 不是约束(它不在类型中 => 箭头的左侧),而是普通类型。 a :~: b 是用作 运行时 证明类型 a 等于类型 b 的值的类型。如果实际上它们 相等,您的程序将不会在您表达类型 a :~: b 之前编译失败;相反,您只是无法真正得出该类型的值(底部除外)。

特别是,a :~: b 是一种具有数据构造函数的类型:Refl。要有效地使用它,您通常需要一个 a :~: b 作为参数。然后对其进行模式匹配。在模式匹配的范围内(即 case 语句的主体),编译器将使用两种类型相等的假设。由于 bottom 值的模式匹配永远不会成功(它可能会抛出异常,或者它可能会永远计算),事实上你总是可以提供 bottom 作为 a :~: b 的“证明”实际上不会造成大问题;你可以对编译器撒谎,但它永远不会执行依赖于你的谎言的代码。

与 OP 中的示例相对应的示例是:

foo :: Int :~: Int -> ()
foo proof
  = case proof of
      Refl  -> ()

bar :: Int :~: Bool -> ()
bar proof
  = case proof of
      Refl  -> ()

bar 可以存在,即使它 需要 不可能的证明。我们甚至可以用 bar undefined 之类的东西 调用 bar,利用类型 Int :~: Bool 中的底部值。这在编译时不会被检测为错误,但会抛出运行时异常(如果它实际被评估;惰性评估可能会避免错误)。而 foo 可以简单地用 foo Refl.

调用

:~:(和~)当然在两种类型都是(或包含)变量时更有用,而不是像Int和[=这样简单的具体类型45=]。它也经常与 Maybe 之类的东西结合使用,因此当类型 not 被证明相等时,您就有了一种表达方式。一个稍微不那么简单的例子是:

strange :: Maybe (a :~: b) -> a -> b -> [a]
strange Nothing x _ = [x]
strange (Just Refl) x y
  = [x, y]

strange 采用类型 ab 相等的 maybe-proof,以及每个类型的值。如果maybe是Nothing,那么类型可能不相等,所以我们只能把x放在a的列表中。但是,如果我们得到 Just Refl,那么 ab 实际上是相同的类型( 仅在模式匹配中!),所以它是有效的将 xy 放在同一个列表中。

但这确实显示了 :~: 无法用 ~ 实现的功能。即使我们想传递两个 不同 类型的值,我们仍然可以调用 strange ;我们只是在那种情况下被迫传递 Nothing 作为第一个值(或 Just undefined,但这不会给我们带来任何有用的东西)。它允许我们编写代码来考虑 ab 可以 相等,如果它们实际上不是,则不会强制编译失败。而 a ~ b(在约束中)只允许我们要求它们绝对相等,并且在编译时可证明。


1 其中“普通类型”表示 Type 又名 *.

类型的成员