Haskell 的 'forall' 和 '=>' 之间的关系

Relationship between Haskell's 'forall' and '=>'

我无法理解 Haskell 的 forall=> 之间的关系(和互动)(就此而言 .经常连接他们)。

例如

λ> :t (+)
λ> :t id

给予

(+) :: forall a. Num a => a -> a -> a
id :: forall a. a -> a

虽然我理解这些在这些特定情况下是如何工作的,但我不太愿意将表达式(签名?)forall a. Num a =>forall a. 本身解析为有意义的东西,或者我通常可以在更复杂的上下文中理解。

forall a. Num a =>forall a. 是什么意思?具体来说,forall=>a分别扮演什么角色?

我想如果我们添加隐含的括号会有所帮助:

(+) :: ∀ a . ( Num a => (a -> (a -> a)) )
id :: ∀ a . ( a -> a )

总是与 . 一起出现。它基本上是特殊语法,意思是“. 之间的任何内容都是我想引入以下范围的类型变量”

=> 表示 Idris 所说的隐式函数:Num a 字典 对于 instance Num a,这样的字典是隐式的每当您添加数字时都需要。但是这里的 a 是之前某个 引入的类型变量,还是固定类型,其实并不重要。你也可以

(+) :: Num Int => Int -> Int -> Int

那是多余的,因为编译器 知道 Int 是一个 Num 实例,因此会自动(隐含地!)选择正确的字典。

其实,和[=16=之间并没有特别的关系,只是碰巧经常一起使用而已。


实际上这是一个类型级别的lambda。类型表达式 ∀ a . b 的行为类似于值级别表达式 \a -> b.

由于 forall 问题似乎已经解决,我将尝试稍微解释一下 =>=> 左边的东西是参数,很像 -> 左边的东西。但是你不需要手动应用这些参数,它们只能有特定的类型。

f :: Num a => a -> a

是一个有两个参数的函数:

  1. 一本Num a字典。
  2. 一个a.

当您申请 f 时,您只需提供 a。 GHC 必须提供 Num a。如果它应用于特定的具体类型,如 Int,GHC 知道 Num Int 并且可以在调用站点提供它。否则,它会检查 Num a 是否由某个外部上下文提供并使用该上下文。 Haskell 类型类系统的伟大之处在于它确保任何两个 Num a 字典,无论它们是如何被发现的,都是相同的。因此,字典的来源并不重要——它肯定是正确的。

进一步讨论

我们正在谈论的很多事情并不完全是 Haskell 的一部分,因为它们是 GHC 通过翻译成 GHC 核心,AKA 来解释 Haskell 的方式的一部分系统 FC,是经过充分研究的系统 F 的扩展,又名吉拉德-雷诺兹微积分。 System FC 是一个显式类型的多态 lambda 演算,具有代数数据类型等,但没有类型推断,没有实例解析等。在 GHC 检查 Haskell 代码中的类型后,它通过彻底的机械过程。它可以自信地做到这一点,因为类型检查器 "decorates" 代码包含脱糖器需要探测周围所有词典的所有信息。如果你有一个 Haskell 看起来像

的函数
foo :: forall a . Num a => a -> a -> a
foo x y= x + y

然后这将转化为看起来像

的东西
foo :: forall a . Num a -> a -> a -> a
foo = /\ (a :: *) -> \ (d :: Num a) -> \ (x :: a) -> \ (y :: a) -> (+) @a d x y

/\ 是一个 lambda 类型——它只是一个普通的 lambda 行,除了它接受一个类型变量。 @ 表示将一种类型应用于采用一个的函数。 + 实际上只是一个记录选择器。它从传递给它的字典中选择正确的字段。

(从另一个角度来看,不调用 classes 类型的 "implicit dictionary passing" 实现):


Haskell中的

forall a.的意思是"for every type a".1是引入了一个类型变量,声明剩下的类型表达式有 选择 a.

是有效的

你通常不会在 basic Haskell 中看到它(没有在 GHC 中打开任何扩展),因为它不是必需的;您只需在类型签名中使用类型变量,GHC 会自动假定在表达式的开头有 foralls 引入这些变量。

例如:

zip :: forall a. ( forall b. ( [a] -> [b] -> [(a, b)] ))
zip :: forall a. forall b. [a] -> [b] -> [(a, b)]
zip :: forall a b. [a] -> [b] -> [(a, b)]
zip :: [a] -> [b] -> [(a, b)]

以上都是一样的;他们只是告诉我们 zip 可以是一种压缩 a 列表和 b 列表以制作 (a, b) 对列表的方法,无论我们想为 ab.

做出什么选择

forall 主要与扩展一起发挥作用,因为这样你就可以引入范围为 other 的类型变量,而不是 GHC 假定的默认范围,如果你不明确的话写他们。


现在,constraints => type 语法可以大致理解为 "these constraints imply this type" 或 "provided these constraints hold, you can use this type"。它一直在使用,即使在没有扩展名的香草 Haskell 中也是如此,因此了解它的含义及其工作原理很重要,而不仅仅是复制粘贴和希望。

=> 箭头允许我们在类型表达式的其余部分中声明一组对变量的约束;它让我们限制可以做出哪些选择来引入类型变量。您应该先阅读它,忽略 => 箭头左侧的所有内容,然后单独阅读右侧部分。这为您提供了 "shape" 类型。 => 箭头左侧的内容告诉您可以使用哪些类型来使用其余类型。

一个例子:

(+) :: Num a => a -> a -> a

这意味着 (+)a -> a -> a 除了 Num a => 等更简单的类型完全相同告诉我们我们不能自由选择 any 类型 a。当我们知道它是 Num 类型 class 的成员时,我们只能为 a 选择一个类型(另一种稍微更精确的说法是“a 是一个成员Num 的 "the constraint Num a holds").

请注意,GHC 仍然假设这里有一个隐式 forall a 来引入类型变量 a,所以它看起来确实像:

(+) :: forall a. Num a => a -> a -> a

在这种情况下,一旦您知道 forall a.Num a => 的含义,您就可以将其作为英文句子轻松阅读:"For every type a, provided Num a holds, plus has the type a -> a -> a".


1 如果你完全熟悉形式逻辑,它只是一种 ASCII 友好的写法 ∀a, a "universally quantified variable".