Haskell/GHC中的`forall`关键字是什么作用?

381

我开始理解如何在所谓的“存在类型”中使用forall关键字,例如:

data ShowBox = forall s. Show s => SB s

然而,这只是 forall 的一部分用法,我无法理解它在类似以下情况中的使用:

runST :: forall a. (forall s. ST s a) -> a

或者解释为什么它们不同:

foo :: (forall a. a -> a) -> (Char, Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))

或者整个 RankNTypes 的东西……

我倾向于使用清晰、无术语的英语,而不是在学术环境中常见的那种语言。我试图阅读关于此问题的大多数解释(我可以通过搜索引擎找到的那些)都有以下问题:

  1. 它们不完整。它们解释了该关键字的一部分用法(例如“存在类型”),这让我感到高兴,直到我阅读使用完全不同方式的代码时(例如上面的runSTfoobar)。
  2. 它们密集地包含了一些假设,即我已经阅读了本周流行的离散数学、范畴论或抽象代数分支的最新内容。(如果我再也不看到“请参阅实现细节的文献whatever”这样的话,那就太好了。)
  3. 它们的写作方式经常将甚至简单的概念变成扭曲且语义混乱的句子。

所以......

接下来是实际问题。有没有人能以清晰、简单的英语(或者如果存在的话,指出我错过的这样一个清晰的解释),完全解释forall关键字,而不假设我是一个深谙行话的数学家?


11
这个主题在Haskell wiki上看起来相当适合初学者。 - jhegedus
https://wasp-lang.dev/blog/2021/09/01/haskell-forall-tutorial 是一篇相当易读的概述文章(看起来是受这个SO问题的启发!) - unhammer
8个回答

320

让我们从一个代码示例开始:

foob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> b
foob postProcess onNothin onJust mval =
    postProcess val
    where
        val :: b
        val = maybe onNothin onJust mval

这段代码在普通的Haskell 98中无法编译(语法错误),需要扩展支持forall关键字。

基本上,forall关键字有三个不同的常见用途(至少看起来是这样),每个用途都有自己的Haskell扩展:ScopedTypeVariablesRankNTypes/Rank2TypesExistentialQuantification

上面的代码在启用它们中的任何一个时都不会出现语法错误,但只有在启用ScopedTypeVariables时才能通过类型检查。

作用域类型变量:

作用域类型变量帮助指定where子句内部代码的类型。它使得val :: b中的bfoob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> b中的b相同。

一个令人困惑的点:您可能会听说,当您从类型中省略forall时,它实际上仍然隐含存在。(来自Norman的答案:"通常这些语言从多态类型中省略了forall")。这个说法是正确的,但它指的是forall的其他用途,而不是ScopedTypeVariables的用途。

Rank-N-Types:

让我们从mayb :: b -> (a -> b) -> Maybe a -> b等价于mayb :: forall a b. b -> (a -> b) -> Maybe a -> b开始,除非启用了ScopedTypeVariables

这意味着它适用于每个ab

假设您想要做这样的事情。

ghci> let putInList x = [x]
ghci> liftTup putInList (5, "Blah")
([5], ["Blah"])

这个 liftTup 的类型必须是什么?它的类型是liftTup :: (forall x. x -> f x) -> (a, b) -> (f a, f b)。为了理解为什么,我们试着编写它:

ghci> let liftTup liftFunc (a, b) = (liftFunc a, liftFunc b)
ghci> liftTup (\x -> [x]) (5, "Hello")
    No instance for (Num [Char])
    ...
ghci> -- huh?
ghci> :t liftTup
liftTup :: (t -> t1) -> (t, t) -> (t1, t1)

"嗯...为什么GHC会推断这个元组必须包含两个相同的类型?让我们告诉它它们不必如此"

-- test.hs
liftTup :: (x -> f x) -> (a, b) -> (f a, f b)
liftTup liftFunc (t, v) = (liftFunc t, liftFunc v)

ghci> :l test.hs
    Couldnt match expected type 'x' against inferred type 'b'
    ...

嗯。所以在这里,GHC 不允许我们在 v 上应用 liftFunc,因为 v :: bliftFunc 需要一个 x。我们真正想要的是我们的函数能够接受任何可能的 x 的函数!

{-# LANGUAGE RankNTypes #-}
liftTup :: (forall x. x -> f x) -> (a, b) -> (f a, f b)
liftTup liftFunc (t, v) = (liftFunc t, liftFunc v)

因此,并非所有的x都适用于liftTup,而是它所获得的函数适用于所有的x

存在量词:

让我们举个例子:

-- test.hs
{-# LANGUAGE ExistentialQuantification #-}
data EQList = forall a. EQList [a]
eqListLen :: EQList -> Int
eqListLen (EQList x) = length x

ghci> :l test.hs
ghci> eqListLen $ EQList ["Hello", "World"]
2

这与Rank-N-Types有何不同?
ghci> :set -XRankNTypes
ghci> length (["Hello", "World"] :: forall a. [a])
    Couldnt match expected type 'a' against inferred type '[Char]'
    ...

使用Rank-N-Types,forall a 表示你的表达式必须适合所有可能的 as。例如:
ghci> length ([] :: forall a. [a])
0

一个空列表可以作为任何类型的列表。

因此,在存在量化中,data 定义中的 forall 意味着,包含的值可以任何适当的类型,而不是必须是所有适当的类型。


5
好的,我已经睡足了六个小时,现在能够理解你的回答了。 :) 在你和诺曼之间,我得到了正是我所寻找的答案类型。谢谢。 - JUST MY correct OPINION
2
实际上,你让 ScopedTypeVariables 看起来比它实际上更糟糕。如果你使用这个扩展写出类型为 b -> (a -> b) -> Maybe a -> b 的代码,它仍然完全等同于 forall a b. b -> (a -> b) -> Maybe a -> b。但是,如果你想要引用相同的 b(而不是隐式量化它),那么你需要写明确量化的版本。否则,STV 将是一个极其侵入性的扩展。 - nominolo
1
@nominolo:我并不是想贬低ScopedTypeVariables,也不认为它有什么不好。在我看来,它是编程过程中非常有用的工具,特别是对于Haskell初学者来说,我很感激它的存在。 - yairchu
2
这是一个比较老的问题(和答案),但是更新一下以反映存在类型可以使用GADTs表达的事实可能是值得的,用这种方法量化会更容易理解(至少对我来说是这样)。 - dfeuer
1
我个人认为,用 GADT 形式来解释/理解存在符号比单独解释更容易,但你当然可以有不同的看法。 - dfeuer
显示剩余2条评论

146

有人能用简单明了的英语完全解释forall关键字吗?

没有。(也许唐·斯图尔特可以。)

以下是阻碍简单明了解释forall的障碍:

  • 它是一个量词。你必须至少了解一点逻辑(谓词演算)才能看到全称量词或存在量词。如果你从未接触过谓词演算或对量词不熟悉(我曾见过在博士资格考试中对此不熟悉的学生),那么对于你来说,forall没有简单的解释。

  • 它是一个类型量词。如果你没有看过System F并练习过编写多态类型,你会发现forall很困惑。虽然有Haskell或ML的经验,但通常这些语言省略多态类型中的forall。(在我看来,这是一种语言设计错误。)

  • 特别是在Haskell中,forall的使用方式让我感到困惑。(我不是类型理论家,但我的工作让我接触到大量的类型理论,并且我对它非常熟悉。)对我来说,主要的困惑来源是forall用于编码我自己更喜欢使用exists编写的类型。它通过量词和箭头的棘手的类型同构来证明,并且每次我想要理解它时,我都必须查找信息并自己计算同构。

    如果你对类型同构的概念不熟悉,或者如果你没有任何练习思考类型同构,那么这种使用forall的方式会阻碍你。

  • 虽然forall的一般概念总是相同的(绑定引入类型变量),但不同用途的细节可能会有很大的差异。非正式的英语不是解释这些变化的很好的工具。要真正理解发生了什么,您需要一些数学知识。在这种情况下,相关的数学可以在本杰明·皮尔斯的入门教材《类型和编程语言》中找到,这是一本非常好的书。

关于你提出的具体例子,

  • runST should make your head hurt. Higher-rank types (forall to the left of an arrow) are rarely found in the wild. I encourage you to read the paper that introduced runST: "Lazy Functional State Threads". This is a really good paper, and it will give you a much better intuition for the type of runST in particular and for higher-rank types in general. The explanation take several pages, it's very well done, and I'm not going to try to condense it here.

  • Consider

    foo :: (forall a. a -> a) -> (Char,Bool)
    bar :: forall a. ((a -> a) -> (Char, Bool))
    

    If I call bar, I can simply pick any type a that I like, and I can pass it a function from type a to type a. For example, I can pass the function (+1) or the function reverse. You can think of the forall as saying "I get to pick the type now". (The technical word for picking the type is instantiating.)

    The restrictions on calling foo are much more stringent: the argument to foo must be a polymorphic function. With that type, the only functions I can pass to foo are id or a function that always diverges or errors, like undefined. The reason is that with foo, the forall is to the left of the arrow, so as the caller of foo I don't get to pick what a is—rather it's the implementation of foo that gets to pick what a is. Because forall is to the left of the arrow, rather than above the arrow as in bar, the instantiation takes place in the body of the function rather than at the call site.

概述:对于forall关键字的完整解释需要数学知识,只有学过数学的人才能理解。即使是部分解释也很难在没有数学基础的情况下理解。但也许我的非数学的部分解释会有所帮助。去阅读Launchbury和Peyton Jones关于runST的内容吧!


附录: 术语“above”、“below”和“to the left of”与类型的文本书写方式无关,而是与抽象语法树有关。在抽象语法中,forall接受类型变量的名称,然后在其下方有一个完整的类型。箭头接受两个类型(参数类型和结果类型)并形成一个新类型(函数类型)。参数类型位于箭头“左侧”,它是抽象语法树中箭头的左子节点。

示例:

  • In forall a . [a] -> [a], the forall is above the arrow; what's to the left of the arrow is [a].

  • In

    forall n f e x . (forall e x . n e x -> f -> Fact x f) 
                  -> Block n e x -> f -> Fact x f
    

    the type in parentheses would be called "a forall to the left of an arrow". (I'm using types like this in an optimizer I'm working on.)


12
谢谢,但我正在为后人写作。我遇到过不止一个程序员认为 forall a . [a] -> [a] 中的 forall 在箭头的左侧。 - Norman Ramsey
13
我读了左边,然后我看了看实际的左边。直到你说“语法树”,我才明白了这句话的意思。 - Paul Nathan
感谢指出皮尔斯的书。它对系统F有非常清晰的解释。它解释了为什么“exists”从未被实现。(它不是系统F的一部分!)在Haskell中,系统F的一部分被隐含了,但“forall”是不能完全掩盖的东西之一。就好像他们从Hindley-Milner开始,允许“forall”被隐式地表示,然后选择了更强大的类型系统,这让那些学习FOL的“forall”和“exists”的人感到困惑并停在那里。 - T_S_
整个 lens 生态系统以及其基于 Van Laarhoven 镜头的实现都严重依赖于二阶类型。一个 Lens s t a b 是一个在 Functor 中具有多态性的函数。 - dfeuer
@T_S_ 这正是发生的事情。最初的 Haskell(Haskell98)是基于 Hindley-Milner 和类型类(https://www.haskell.org/onlinereport/decls.html)进行规定的,但通过一系列的语言扩展,GHC 基于 System F(确切地说是 System FC)构建了一个更强大的类型系统(https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/compiler/fc)。 - Ari Fordsham
显示剩余3条评论

61

请问有谁能用简单易懂的英语完全解释一下'forall'关键字吗?正如Norman所说,从类型理论中提取技术术语并给出简单易懂的解释是很困难的。但我们都在尝试着。

'forall'只需要记住一个要点:它将类型绑定到某个范围。一旦你理解了这一点,一切就变得相对容易了。在类型级别上,'forall'类似于lambda(或let的一种形式)-- Norman Ramsey在他的优秀回答中使用"左侧"/"上方"的概念来传达这种作用域的概念。

'forall'的大多数用法都很简单,您可以在GHC用户手册第7.8节中找到介绍,特别是非常好的第7.8.5节,介绍了嵌套形式的'forall'。

在Haskell中,当类型被普遍量化时,我们通常会省略类型绑定符:

length :: forall a. [a] -> Int

等同于:

length :: [a] -> Int

就是这样。

现在你可以将类型变量绑定到某些作用域中,因此你可以有不同于顶层的作用域 ("普遍量化"),就像你的第一个例子一样,其中类型变量仅在数据结构内可见。这允许隐藏类型 ("存在类型")。或者我们可以有绑定任意嵌套("排名N类型")。

要深入了解类型系统,您需要学习一些术语。这是计算机科学的本质。但是,像上面那样的简单用法应该能够通过与值级别上的“let”进行类比来直观地理解。一个很好的介绍是Launchbury和Peyton Jones


5
启用ScopedTypeVariables时,从技术上讲,length :: forall a. [a] -> Intlength :: [a] -> Int不等同。当存在forall a.时,它会影响lengthwhere子句(如果有的话),并更改其中命名为a的类型变量的含义。 - yairchu
3
确实。ScopedTypeVariables 使得情况变得有点复杂。 - Don Stewart
4
在您的解释中,"it binds types to some scope" 可以更好地表述为 "它将类型变量绑定到某个作用域"。 - Romildo

37

这里是一个简单易懂的解释,你可能已经熟悉。

forall 关键字在 Haskell 中只有一种用法,每次看到它时都表示相同的含义。

全称量词

形如 forall a. f a 的类型被称作全称量词类型。这种类型的值可以看作是一个函数,它接受一个类型 a 作为参数,并返回一个类型为 f a 的值。但在 Haskell 中,这些类型参数是由类型系统隐式传递的。这个“函数”必须无论收到哪种类型的参数都返回相同的值,因此该值是多态的

例如,考虑类型 forall a. [a]。这种类型的值接受另一种类型 a,并返回一个由相同类型 a 的元素组成的列表。当然,实现方式只有一种。它需要返回空列表,因为 a 可以是任何类型。空列表是唯一具有多态元素类型(因为它没有元素)的列表值。

或者考虑类型 forall a. a -> a。这种类型的函数调用者提供了一个类型 a 和一个类型为 a 的值。然后该函数需要返回同一类型 a 的值。这里也只有一种可能的实现方式,即返回它接收到的相同的值。

存在量词

如果 Haskell 支持该符号,则存在量词类型将采用形如 exists a. f a 的格式。这种类型的值可以看作是一对(或“积”),由类型 a 和类型为 f a 的值组成。

例如,如果你有一个类型为 exists a. [a] 的值,则你拥有一个元素类型未知的列表。它可以是任何类型,但即使你不知道它是什么,你仍然可以对其执行许多操作。你可以反转它,也可以计算元素数量,或者执行任何其他不依赖于元素类型的列表操作。

好的,那么等等。为什么 Haskell 使用 forall 来表示以下“存在”类型呢?

data ShowBox = forall s. Show s => SB s

虽然会有些困惑,但它实际上是在描述数据构造函数SB的类型。

SB :: forall s. Show s => s -> ShowBox

构造完成后,您可以将类型为ShowBox的值视为由两个部分组成。它是一个类型s和一个类型s的值。换句话说,它是存在量化类型的值。ShowBox实际上可以被写作exists s. Show s => s,如果Haskell支持该表示法。

runST及其相关函数

鉴于此,这些有何不同?

foo :: (forall a. a -> a) -> (Char,Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))

首先让我们看看bar。它接受类型a和一个类型为a -> a的函数,并生成一个(Char, Bool)类型的值。例如,我们可以选择将Int作为a并给它一个类型为Int -> Int的函数。但是foo是不同的。它要求foo的实现能够向我们提供任何类型的函数。因此,我们唯一合理的选择就是使用id函数。

现在我们应该能够理解runST类型的含义了:

runST :: forall a. (forall s. ST s a) -> a
因此,无论我们给定什么类型作为arunST都必须能够产生类型为a的值。为了做到这一点,它使用了一个类型为forall s. ST s a的参数,该参数肯定必须以某种方式产生a。此外,它必须能够产生类型为a的值,无论runST的实现决定以什么类型给出s。因此,runST函数实际上是在告诉你:“只要我可以选择类型s,你就可以选择类型a。”好吧,那又怎样?好处是这对runST的调用方施加了约束,即类型a不能涉及类型s,因为你不知道s的类型是什么。例如,你不能传递一个类型为ST s [s]的值。在实践中,这意味着runST的实现可以知道涉及类型s的任何值都是局部于其自身的实现,因此它可以自由地进行一些本来不被允许的操作,例如直接在原地改变值。该类型保证了变异仅在runST的实现中是局部的。runST函数的类型是一个Rank-2多态类型的例子,因为其参数的类型包含一个forall量化器。以上的foo类型也是Rank-2类型。普通的多态类型,如bar的类型,具有等级1,但如果要求参数类型必须是多态的,并且具有自己的forall量化器,则它将成为Rank-2类型。如果一个函数采用Rank-2参数,则其类型为Rank-3,依此类推。通常,一个接受等级为n的多态参数的类型具有等级n + 1

33
他们充斥着我已经阅读了本周流行的离散数学、范畴论或抽象代数的分支的假设。(如果我再也不看到"有关实现细节,请参阅文献"这句话,那就太好了。)
额,那么一阶逻辑呢?forall很明显是指全称量词,在这种情况下,术语存在量词更有意义,但如果有一个exists关键字会更好。量化是有效的普遍还是存在取决于量化器相对于函数箭头的放置位置以及变量在函数箭头两侧使用的方式,这有点令人困惑。
因此,如果这并没有帮助,或者如果您只是不喜欢符号逻辑,从更功能编程的角度来看,您可以将类型变量视为函数的(隐式)类型参数。在这种意义上,以大写lambda写入接受类型参数的函数是传统的方式,我将在这里写成/\
所以,考虑id函数:
id :: forall a. a -> a
id x = x

我们可以将其重写为lambda表达式,将"类型参数"移出类型签名,并添加内联类型注释:
id = /\a -> (\x -> x) :: a -> a

这是对const做的同样的事情:

const = /\a b -> (\x y -> x) :: a -> b -> a

因此,您的bar函数可能是这样的:

bar = /\a -> (\f -> ('t', True)) :: (a -> a) -> (Char, Bool)

请注意,作为参数传递给bar的函数的类型取决于bar的类型参数。考虑如果您有像这样的东西:
bar2 = /\a -> (\f -> (f 't', True)) :: (a -> a) -> (Char, Bool)

这里的bar2将函数应用于类型为Char的某些内容,因此给bar2任何类型参数都会导致类型错误。

另一方面,这是foo可能看起来像什么:

foo = (\f -> (f Char 't', f Bool True))

bar不同,foo实际上根本不需要任何类型参数!它接受一个函数,该函数本身接受一个类型参数,然后将该函数应用于两个不同的类型。

因此,当您在类型签名中看到forall时,只需将其视为类型签名的lambda表达式。就像常规lambda一样,forall的作用范围尽可能向右延伸,直到封闭括号,并且就像在常规lambda中绑定的变量一样,由forall绑定的类型变量仅在量化表达式内部范围内。


< p > 后记:或许你会想——既然我们正在思考接受类型参数的函数,为什么不能在这些参数中做更有趣的事情呢? 答案是我们可以!

将类型变量与标签组合并返回新类型的函数称为类型构造函数,你可以编写类似以下代码:

Either = /\a b -> ...

但我们需要全新的符号表示,因为这种类型的写法,例如Either a b,已经暗示了“将函数Either应用于这些参数”的意思。
另一方面,一种在其类型参数上进行“模式匹配”,针对不同类型返回不同值的函数是一个类型类的方法。对我上面的/\语法进行轻微扩展,可以得出以下内容:
fmap = /\ f a b -> case f of
    Maybe -> (\g x -> case x of
        Just y -> Just b g y
        Nothing -> Nothing b) :: (a -> b) -> Maybe a -> Maybe b
    [] -> (\g x -> case x of
        (y:ys) -> g y : fmap [] a b g ys 
        []     -> [] b) :: (a -> b) -> [a] -> [b]

个人而言,我认为我更喜欢 Haskell 的实际语法...

一个“模式匹配”其类型参数并返回任意现有类型的函数被称为类型族函数依赖--在前一种情况下,它甚至已经非常像函数定义。


1
这里提供了一个有趣的观点。这为我在解决问题时提供了另一个攻击角度,从长远来看可能会非常有成效。谢谢。 - JUST MY correct OPINION
@KennyTM:或者说λ,但是 GHC 的 Unicode 语法扩展不支持它,因为 λ 是一个字母,这是一个不幸的事实,假设也适用于我的大型 lambda 抽象。因此,通过类比\,使用 /\。我想我本可以使用 ,但我试图避免谓词演算... - C. A. McCann

15

有没有人能够用清晰简明的英语完全解释forall关键字的含义(或者,如果存在这样一个清晰的解释,指向我错过的这样一个清晰的解释)?不要假设我是一个深谙术语的数学家。

我将尝试解释Haskell及其类型系统中的forall的含义和应用。

但在您理解之前,我想引您观看Runar Bjarnason的一次非常易懂且妙趣横生的演讲,标题为“Constraints Liberate, Liberties Constrain”。演讲充满了来自真实世界使用案例的示例,以及Scala中的示例支持该语句,尽管它没有提到forall。我将在下面尝试解释forall的角度。

                CONSTRAINTS LIBERATE, LIBERTIES CONSTRAIN

非常重要的是要理解并相信这个陈述,以便进行以下说明,因此我敦促您观看演讲(至少部分内容)。

现在一个非常常见的例子,展示了Haskell类型系统的表现力,就是这个类型签名:

foo :: a -> a

据说在给定这个类型签名的情况下,只有一个函数可以满足这个类型,那就是identity函数或者更为流行的id函数。

在我学习Haskell的初期,我总是想知道下面的函数:

foo 5 = 6

foo True = False

如果它们都符合上述类型签名,那么为什么Haskell的开发者声称只有id符合类型签名呢?这是因为在类型签名中隐藏了一个隐式的forall。实际类型为:
id :: forall a. a -> a

现在我们回到这个说法:约束使自由,自由限制

将其翻译成类型系统,该语句变为:

类型级别上的约束,成为项级别上的自由

以及

类型级别上的自由,成为项级别上的约束


现在,让我们看一下使用foo函数的第一个语句:
类型级别的限制...
因此,在我们的类型签名中加上一个限制。
foo :: (Num a) => a -> a

在术语层面上变得自由,使我们有自由或灵活性来编写所有这些内容。

foo 5 = 6
foo 4 = 2
foo 7 = 9
...

当使用任何其他类型类约束a时,也可以观察到相同的情况。

因此,现在这个类型签名:foo :: (Num a) => a -> a 的翻译是:

∃a , st a -> a, ∀a  Num

这被称为存在量化,翻译成“存在着”一些a的实例,当给定类型为a的输入时,函数返回相同类型的输出,并且这些实例都属于数字集合。
因此,我们可以看到添加一个限制条件(即a应属于数字集合),使术语级别具有多种可能的实现。
现在来看第二个语句,这实际上解释了forall

类型级别的自由,在项级别上变成了约束。

因此,现在让我们在类型级别上解放函数:
foo :: forall a. a -> a

现在这个翻译为:
∀a , a -> a

这意味着该类型签名的实现在所有情况下都应该是a -> a。因此,这从术语层面开始限制我们。我们不能再写:
foo 5 = 7

因为如果我们将a作为Bool,这个实现就不会满足要求。 a可以是Char[Char]或自定义数据类型。在所有情况下,它都应返回类似类型的内容。这种在类型层面上的自由称为通用量化,唯一能够满足此要求的函数是:

foo a = a

这通常被称为身份函数


因此,forall是类型级别上的一种自由度,其实际目的是将术语级别限制在特定实现中。

9
这个关键字有不同用法的原因是它实际上在至少两种类型系统扩展中使用:高阶类型和存在类型。最好单独了解这两个东西,而不是试图同时解释为什么“forall”在两者中都是适当的语法位。

4

存在性量词是什么?

在存在性量化中,data 定义中的 forall 表示所包含的值可以是任何适合的类型,而不是必须是所有适合的类型。 -- yachiru's answer

关于为什么 data 定义中的 forall 等同于 (exists a. a)(伪 Haskell)的解释,请参阅wikibooks's "Haskell/Existentially quantified types"

以下是简要摘录:

data T = forall a. MkT a -- an existential datatype
MkT :: forall a. a -> T -- the type of the existential constructor

当模式匹配/解构 MkT x 时,x 的类型是什么?
foo (MkT x) = ... -- -- what is the type of x?

x可以是任何类型(如forall中所述),因此它的类型为:

x :: exists a. a -- (pseudo-Haskell)

因此,以下内容是同构的:
data T = forall a. MkT a -- an existential datatype
data T = MkT (exists a. a) -- (pseudo-Haskell)

forall意味着所有

我对所有这一点的简单解释是,"forall实际上意味着 '所有' "。 需要明确的一个重要区别是forall对于定义和函数应用的影响。

forall意味着值或者函数的定义必须是多态的。

如果被定义的东西是多态的,那么它意味着该值必须适用于所有合适的a,这是相当严格的。

如果被定义的东西是多态的函数,那么它意味着该函数必须适用于所有合适的a,这并不那么严格,因为函数是多态的并不意味着应用到函数中的参数必须是多态的。也就是说,如果该函数适用于所有的a,那么反过来,任何合适的a都可以应用到该函数中。但是,在函数定义中只能选择一次参数的类型。

如果forall出现在函数参数类型中(即Rank2Type),那么它意味着应用的参数必须是真正的多态,以保持forall意味着定义是多态的这一思想的一致性。在这种情况下,参数的类型可以在函数定义中选择多次(如Norman所指出的"并由函数的实现选择")。
因此,存在类型的data定义允许a为任何值的原因是因为数据构造函数是一个多态的函数。
MkT :: forall a. a -> T

类型为 MkT :: a -> *

这意味着任何 a 都可以应用于该函数。与多态的相反:

valueT :: forall a. [a]

值的类型为 a

这意味着 valueT 的定义必须是多态的。在这种情况下,valueT 可以被定义为空列表 [],适用于所有类型。

[] :: [t]

区别

尽管在ExistentialQuantificationRankNType中,forall的含义是一致的,但existentials存在差异,因为data构造函数可以用于模式匹配。正如ghc用户指南中所记录的:

在模式匹配时,每个模式匹配为每个存在类型变量引入了一个新的、独立的类型。这些类型不能与任何其他类型统一,也不能从模式匹配的范围中逃脱。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接