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

时间:2010-06-18 15:50:01

标签: haskell syntax types ghc forall

我开始明白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. 他们不完整。他们解释了使用这个关键字的一部分(比如“存在类型”),这让我感到高兴,直到我读到以完全不同的方式使用它的代码(如runSTfoo和{{ 1}}上面)。
  2. 他们密集的假设我已经阅读了最新的任何分支的离散数学,类别理论或抽象代数本周流行。 (如果我再也没有读过“查阅论文无论 ”的话,那将会很快。)
  3. 他们的编写方式经常将简单的概念转变为曲折的语法和语法。
  4. 因此...

    关于实际问题。任何人都可以用清晰,简单的英语完全解释bar关键字(或者,如果它存在于某个地方,指向我错过的这样一个明确的解释),这并不能假设我是一个沉浸在行话中的数学家?


    已编辑添加:

    下面有高质量的答案有两个突出的答案,但不幸的是我只能选择一个最好的答案。 Norman's answer详细而有用,以一种方式解释事物,显示了forall的一些理论基础,同时向我展示了它的一些实际意义。 yairchu's answer覆盖了其他人没有提及的范围(范围类型变量),并用代码和GHCi会话说明了所有概念。我愿意选择两者是最好的。不幸的是,我不能并且在仔细查看两个答案之后,我已经确定yairchu略微偏离Norman的,因为说明性代码和附加说明。然而,这有点不公平,因为我真的需要两个答案才能理解这一点,forall当我在类型签名中看到它时,我不会留下一丝恐惧感。

8 个答案:

答案 0 :(得分:240)

让我们从代码示例开始:

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关键字有3种不同的常用用法(或者至少是似乎),并且每种都有自己的Haskell扩展名: ScopedTypeVariablesRankNTypes / Rank2TypesExistentialQuantification

上面的代码没有启用任何一个语法错误,但只启用了ScopedTypeVariables的类型检查。

范围类型变量:

Scoped类型变量有助于为where子句中的代码指定类型。它使b中的val :: bb中的foob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> b相同。

令人困惑的一点:您可能会听到当您从类型中省略forall时,它实际上仍然隐含在那里。 (from Norman's answer: "normally these languages omit the forall from polymorphic types")。此声明是正确的,它指的是forall的其他用途,而不是ScopedTypeVariables使用。

<强>秩-N-类型:

首先,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不允许我们在liftFunc上应用v,因为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)

所以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 ,而是它所获得的功能。

存在量化:

让我们举个例子:

ghci> :set -XRankNTypes
ghci> length (["Hello", "World"] :: forall a. [a])
    Couldnt match expected type 'a' against inferred type '[Char]'
    ...

与Rank-N-Types有什么不同?

forall a

对于Rank-N-Types,a意味着您的表达式必须适合所有可能的ghci> length ([] :: forall a. [a]) 0 。例如:

forall

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

因此,通过存在量化,data定义中的{{1}} s表示包含的值可以是任何合适的类型,而不是它必须属于所有合适的类型。

答案 1 :(得分:110)

  

任何人完全可以用清晰明白的英语解释forall关键字吗?

不。(好吧,也许Don Stewart可以。)

以下是简单明了的解释或forall

的障碍
  • 这是量词。你必须至少有一点逻辑(谓词演算)才能看到一个普遍的或存在的量词。如果你从未见过谓词演算或对量词不熟悉(我在博士资格考试中看到学生不舒服),那么对你来说,forall没有简单的解释。

    < / LI>
  • 这是类型量词。如果您还没有看到System F并且已经开始编写多态类型的练习,那么您会发现forall令人困惑。使用Haskell或ML的经验是不够的,因为通常这些语言会从多态类型中省略forall。 (在我看来,这是一个语言设计错误。)

  • 特别是在Haskell中,forall的使用方式让我感到困惑。 (我不是一个类型理论家,但是我的工作让我接触到类型理论的很多,而且我对它很满意。)对我来说,混淆的主要原因是forall用于编码我自己更愿意使用exists编写的类型。这是一个涉及量词和箭头的类型同构的一个棘手的问题,并且每当我想要理解它时,我必须自己查找并解决同构现象。

    如果你对类型同构的想法不满意,或者你没有任何关于类型同构的实践,那么forall的这种使用会阻碍你。

  • 虽然forall的一般概念始终相同(绑定以引入类型变量),但不同用途的细节可能会有很大差异。非正式英语不是解释变化的非常好的工具。要真正了解正在发生的事情,你需要一些数学知识。在这种情况下,相关的数学可以在Benjamin Pierce的介绍性文本类型和编程语言中找到,这是一本非常好的书。

至于你的具体例子,

  • runST 应该让你的头受伤。在野外很少发现较高级别的类型(箭头左侧的forall)。我们建议您阅读介绍runST"Lazy Functional State Threads"的论文。这是一篇非常好的论文,它将为您提供更好的直觉,尤其是runST的类型以及更高级别的类型。这个解释需要几页,它做得很好,我不打算在这里压缩它。

  • 考虑

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

    如果我致电bar,我只需选择我喜欢的任何类型a,我就可以将其从a类型传递给a类型。例如,我可以传递函数(+1)或函数reverse。您可以将forall视为“我现在可以选择类型”。 (选择类型的技术词是实例化。)

    调用foo的限制要严格得多:foo 的参数必须是一个多态函数。对于该类型,我可以传递给foo的唯一函数是id或者总是出现偏差或错误的函数,例如undefined。原因是,对于fooforall位于箭头的左侧,因此foo的来电者我不会选择a - 而foo实现可以选择a。因为forall位于箭头左侧,而不是bar中的箭头上方,所以实例化发生在函数体内而不是调用站点。

摘要forall关键字的完整说明需要数学,只有研究过数学的人才能理解。没有数学,即使是部分解释也很难理解。但也许我的部分非数学解释有点帮助。在runST上阅读Launchbury和Peyton Jones!


附录:术语“上方”,“下方”,“左侧”。这些与编写类型的 textual 方式无关,而与抽象语法树有关。在抽象语法中,forall采用类型变量的名称,然后在forall下面有一个完整类型。箭头采用两种类型(参数和结果类型)并形成一种新类型(函数类型)。参数类型是“箭头左侧”;它是抽象语法树中箭头的左子项。

示例:

  • forall a . [a] -> [a]中,forall在箭头上方;箭头左侧的内容是[a]

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

    括号中的类型将被称为“箭头左侧的”。 (我在我正在使用的优化器中使用这样的类型。)

答案 2 :(得分:48)

我原来的回答:

  

任何人都可以用清晰明白的英语完整地解释forall关键字

正如诺曼所指出的那样,很难给出一个明确的,简单的英语解释 类型理论的技术术语。我们都在尝试。

关于'forall'只有一件事需要记住:它将类型绑定到 一些范围。一旦你理解了,一切都相当容易。它是 等同于'lambda'(或'let'的一种形式)在类型级别 - Norman Ramsey 使用“左”/“上方”的概念在his excellent answer中传达同样的范围概念。

'forall'的大多数用法非常简单,您可以在the GHC Users Manual, S7.8中找到它们,特别是嵌套在the excellent S7.8.5上 'forall'的形式。

在Haskell中,当类型为时,我们通常会忽略类型的绑定器 普遍定量化,如下:

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

相当于:

length :: [a] -> Int

就是这样。

由于您现在可以将类型变量绑定到某个范围,因此可以使用其他范围 比最高级别(“universally quantified”),就像你的第一个例子, 其中type变量仅在数据结构中可见。这允许 对于隐藏类型(“existential types”)。或者我们可以有arbitrary nesting个绑定(“排名N类型”)。

要深入理解类型系统,您需要学习一些术语。那是 计算机科学的本质。但是,如上所述,应该是简单的用途 能够直观地掌握,通过在价值水平上使用'let'进行类比。一个 好的介绍是Launchbury and Peyton Jones

答案 3 :(得分:27)

  

他们密集的假设我已经阅读了最新的任何分支的离散数学,类别理论或抽象代数本周流行。 (如果我再也没有读过“请参阅论文的任何细节”,那就太早了。)

呃,简单的一阶逻辑怎么样? forall非常清楚地提及universal quantification,在这种情况下,existential这个词也更有意义,但如果有exists个关键字则不那么尴尬。量化是有效的通用还是存在取决于量词相对于函数箭头哪一侧使用变量的位置,这有点令人困惑。

所以,如果这没有帮助,或者如果你只是不喜欢符号逻辑,那么从更具功能性的编程角度来看,你可以认为类型变量只是(隐式的)类型函数的参数。在这种意义上采用类型参数的函数传统上是出于任何原因使用大写lambda编写的,我将在这里写为/\

因此,请考虑id函数:

id :: forall a. a -> a
id x = x

我们可以将它重写为lambda,将“type参数”移出类型签名并添加内联类型注释:

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除了Char之外的任何类型参数都会导致类型错误。

另一方面,foo可能是这样的:

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

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

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


Post scriptum :也许您可能想知道 - 现在我们正在考虑采用类型参数的函数,为什么我们不能使用这些参数做一些更有趣的事情而不是将它们放入类型签名中?答案是我们可以!

将类型变量与标签放在一起并返回新类型的函数是类型构造函数,您可以这样写:

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的实际语法......

“模式匹配”其类型参数并返回任意现有类型的函数是类型族函数依赖 - 在前一种情况下,它甚至已经看起来很像功能定义。

答案 4 :(得分:22)

这是一个简单明了的快速而肮脏的解释,你可能已经熟悉了。

forall关键字实际上只在Haskell中以一种方式使用。当你看到它时,它总是意味着同样的事情。

通用量化

通用量化类型是一种forall a. f a形式。该类型的值可以被视为一个函数,它将类型 a作为其参数,并返回输入f a。除了在Haskell中,这些类型参数由类型系统隐式传递。无论接收哪种类型,此“函数”都必须为您提供相同的值,因此值为多态

例如,考虑类型forall a. [a]。该类型的值采用另一种类型a,并返回相同类型a的元素列表。当然,只有一种可能的实施方式。它必须给你空列表,因为a可以绝对是任何类型。空列表是元素类型中唯一的多态的列表值(因为它没有元素)。

或类型forall a. a -> a。此类函数的调用者提供类型a和类型a的值。然后,实现必须返回相同类型a的值。只有一种可能的实施方式。它必须返回与给定值相同的值。

存在量化

存在量化类型将具有exists a. f a形式,如果Haskell支持该符号。该类型的值可以被视为一对(或“产品”),其由类型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的值。换句话说,它是存在量化类型的值。如果Haskell支持这种表示法,ShowBox可以真正写成exists s. Show s => s

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

因此runST必须能够生成a类型的值,无论我们提供的类型为a。要做到这一点,它需要一个forall s. ST s a类型的参数,它只是类型forall s. s -> (a, s)的函数。然后,无论(a, s)的实现决定以runST的形式提供什么类型,该函数都必须能够生成s类型的值。

好的,那又怎样?好处是,这会对runST的调用者施加约束,因为类型a根本不能涉及类型s。例如,您无法将类型ST s [s]的值传递给它。这在实践中意味着runST的实现可以使用类型s的值自由地执行变异。类型系统保证这种变异是runST的实现的本地变种。

runST的类型是 rank-2多态类型的示例,因为其参数的类型包含forall量词。上面foo的类型也是第2级。普通的多态类型,如bar的类型,是rank-1,但如果参数的类型需要是多态的,它就变为rank-2 ,用他们自己的forall量词。如果一个函数采用rank-2参数,那么它的类型是rank-3,依此类推。通常,采用等级n的多态参数的类型具有等级n + 1

答案 5 :(得分:8)

此关键字有不同用途的原因是它实际上至少用于两种不同类型的系统扩展:更高级别的类型和存在性。

最好只是分别阅读和理解这两件事,而不是试图在同时解释为什么'forall'是一个合适的语法。

答案 6 :(得分:7)

  

任何人都可以用清晰,简单的英语完全解释forall关键字(或者,如果它存在于某个地方,指向我错过的这样一个明确的解释),并不能假设我是数学家沉浸在行话中?

我将尝试在Haskell及其类型系统的上下文中解释forall的含义和可能的应用。

但是在你明白之前我想引导你去Runar Bjarnason谈论一个非常容易接受的好话题,&#34; Constraints Liberate, Liberties Constrain&#34;。虽然它没有提到forall,但这个演讲充满了来自现实世界用例的示例以及Scala中支持此陈述的示例。我将尝试解释下面的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 :: (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应该属于Numbers集合),释放术语级别以具有多个可能的实现。

现在进入第二个陈述和实际带有forall

解释的陈述

类型级别的自由,成为术语级别的约束

现在让我们在类型级别解放函数:

foo :: forall a. a -> a

现在这转换为:

∀a , a -> a

这意味着此类型签名的实现应该是a -> a适用于所有情况。

所以现在这开始限制我们在学期水平。 我们再也不能写了

foo 5 = 7

因为如果我们将a作为Bool,则此实现将无法满足。 a可以是Char[Char]或自定义数据类型。在任何情况下它都应该返回类似的东西。类型级别的这种自由是所谓的通用量化,唯一可以满足这一要求的功能是

foo a = a

通常称为identity函数

因此,forall是类型级别的liberty,其实际目的是constrain特定实现的术语级别。

答案 7 :(得分:2)

存在主义存在主义是什么?

  

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

可以在wikibooks's "Haskell/Existentially quantified types"中找到forall定义中data(exists a. a)(伪Haskell)同构的原因的解释。

以下是简要的逐字摘要:

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定义与功能应用的影响。

forall表示值或函数的定义必须是多态的。

如果被定义的东西是多态的,那么这意味着该值必须对所有合适的a有效,这是非常有限的。

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

如果forall在函数参数的类型中(即Rank2Type),则表示已应用参数必须真正多态,与forall的概念一致,意味着定义是多态的。在这种情况下,可以在函数定义中多次选择参数的类型("and is chosen by the implementation of the function", as pointed out by Norman

因此,存在性data定义允许任何 a的原因是因为数据构造函数是多态函数

MkT :: forall a. a -> T

有点MkT :: a -> *

这意味着任何a都可以应用于该功能。而不是,例如,多态

valueT :: forall a. [a]

有点值T :: a

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

[] :: [t]

的差异

即使forallExistentialQuantificationRankNType的含义一致,但由于data构造函数可用于模式匹配,因此存在性存在差异。如ghc user guide中所述:

  

当模式匹配时,每个模式匹配为每个存在类型变量引入一个新的,不同的类型。这些类型不能与任何其他类型统一,也不能脱离模式匹配的范围。