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

What does the `forall` keyword in Haskell/GHC do?

我开始了解在所谓的"存在主义类型"中如何使用forall关键字,如下所示:

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

然而,这只是forall的使用方式的一个子集,我不能简单地将其用于以下方面:

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

或者解释这些不同的原因:

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

或者整个江户的东西…

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

  • 它们是不完整的。他们解释了使用这个关键字的一部分(如"存在主义类型"),这让我感到高兴,直到我读到以完全不同的方式使用它的代码(如上面的runSTfoobar)。
  • 他们充满了各种假设,我已经阅读了本周流行的离散数学、范畴理论或抽象代数的最新分支。(如果我再也没有读过"咨询论文,无论是什么细节的实现",那就太早了。)
  • 它们的书写方式常常会将简单的概念变成扭曲的、断裂的语法和语义。
  • 所以…

    关于实际问题。有人能用清晰、简单的英语(或者,如果它存在于某个地方,指出我遗漏的如此清晰的解释)完全解释forall关键字,而不认为我是一个沉溺于行话的数学家吗?

    编辑添加:

    下面有两个高质量的答案,但不幸的是,我只能选择一个最好的。诺曼的回答是详细而有用的,以一种方式解释事情,显示了forall的一些理论基础,同时向我展示了它的一些实际含义。Yairchu的答案涵盖了一个没有人提到的领域(范围类型变量),并用代码和GHCI会话说明了所有概念。如果能把两者选得最好,我会的。不幸的是,我不能,而且在仔细研究了这两个答案之后,我决定,因为说明性的代码和附加的解释,亚尔舒的稍微超过诺曼的。然而,这有点不公平,因为我真的需要两个答案来理解这一点,即当我在类型签名中看到它时,forall并不会让我有一丝恐惧。


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

    1
    2
    3
    4
    5
    6
    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的情况下进行类型检查。

    作用域类型变量:

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

    令人困惑的一点是:当您从一个类型中省略forall时,您可能会听到它实际上仍然隐含在那里。(来自诺曼的回答:"通常,这些语言忽略了多态类型中的孔")。该权利要求是正确的,但它是指forall的其他用途,而不是ScopedTypeVariables的用途。

    RANK-N型:

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

    这意味着它适用于每个ab

    假设你想做这样的事情。

    1
    2
    3
    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)。要了解原因,让我们尝试对其进行编码:

    1
    2
    3
    4
    5
    6
    7
    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推断元组必须包含两个相同类型的元组?让我们告诉他们不必

    1
    2
    3
    4
    5
    6
    7
    -- 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的函数!

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

    所以对所有的x来说,不是liftTup起作用,而是它得到的功能。

    存在量化:

    让我们用一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -- 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

    这与n类排名有什么不同?

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

    对于秩n类型,forall a意味着您的表达式必须适合所有可能的as。例如:

    1
    2
    ghci> length ([] :: forall a. [a])
    0

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

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


    Can anybody completely explain the forall keyword in clear, plain English?

    Ok.

    不。(嗯,也许唐·斯图尔特可以。)好的。

    以下是一个简单、清晰的解释或forall的障碍:好的。

    • 它是一个量词。你至少要有一点逻辑(谓词演算)才能看到一个普遍的或存在的量词。如果你从来没有见过谓词演算,或者对量词不满意(我也见过博士资格考试中不舒服的学生),那么对于你来说,对forall没有一个简单的解释。好的。

    • 它是一个类型量词。如果您没有看到系统f,并且进行了一些编写多态类型的练习,那么您会发现forall令人困惑。使用haskell或ml的经验是不够的,因为通常这些语言忽略了多态类型中的forall。(在我看来,这是一个语言设计错误。)好的。

    • 尤其是在Haskell中,forall的使用方式让我感到困惑。(我不是一个类型理论家,但我的工作使我接触到了许多类型理论,我对此相当满意。)对我来说,混淆的主要来源是forall用于编码我自己更喜欢用exists编写的类型。这是有道理的一个棘手的类型同构涉及量词和箭头,每次我想了解它,我必须查找东西,并计算出同构自己。好的。

      如果你对类型同构的概念不满意,或者如果你没有任何关于类型同构的练习,那么使用forall将会使你陷入困境。好的。

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

    至于你的具体例子,好的。

    • runST会使你的头受伤。更高级别的类型(箭头左边的)在野外很少发现。我鼓励您阅读介绍runST的文章:"Lazy函数状态线程"。这是一篇非常好的论文,它将使您对runST的类型,特别是对更高级别的类型,有更好的直觉。这个解释需要几页,做得很好,我不想在这里浓缩。好的。

    • 考虑好的。

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

      如果我bar呼叫,我可以挑任何简单类型a我样,我可以通它一个aa型函数和类型。例如,我可以通(+1)reverse功能或功能。你想说"I get the forall作为一个球员的类型,现在。"(技术)是instantiating采摘Word类型。)

      在呼叫限制在foo是更严格的说法是:必须fooA多态性的功能。那唯一的功能型,I CAN通是一fooid或功能,或diverges样undefined总是错误的。一个原因是,fooforall是左边的箭头,从而为大学foo呼叫者I Don’t去挑什么是a—而它的实现是一个球员afoo会是什么。因为forall是左边的箭头,当箭头在我上面的bar,instantiation发生在身体的功能,而不是在调用站点。

    摘要:一个完整的解释,需要的forall关键字可以只由数学和理解数学研究的人谁。即使是没有讲明白,偏硬的数学。但我偏、不讲数学的帮助一点。读取launchbury和CRP在runST佩顿琼斯!

    附录:术语"以上"、"以下"、"左"。论文有什么做与文本类型是书面的方式和所有的待办事项与抽象语法树。在抽象的语法,forallA一A型变量的名称,然后有一个全型"下面的"forall。在一个箭头(参数类型和结果类型(A型)和新型的功能型)。的参数类型是"左"的箭头的箭头,它的左子树的抽象语法。

    例子:

    • forall a . [a] -> [a],forall是上面的箭头;什么是友好的酒店是[a]左箭头。

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

      在括号可能的类型称为"A forall在靠近左箭头"。(我喜欢这个类型的使用在优化我的工作)。

    好的。


    我的原始答案:

    Can anybody completely explain the forall keyword in clear, plain English

    正如诺曼所指出的,很难给出一个清晰、明了的英语解释。类型理论中的技术术语。不过我们都在努力。

    对于"forall",只需记住一件事:它将类型绑定到一定范围。一旦你明白了这一点,一切就相当容易了。它是在类型级别上等价于"lambda"(或"let"的形式)——Norman Ramsey使用"左"或"上"的概念在HIS中表达相同的范围概念回答得很好。

    "forall"的大多数用法非常简单,您可以在GHC用户手册,S7.8,尤其是嵌套的优秀S7.8.5"forall"的形式。

    在haskell中,当类型为普遍量化,如:

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

    相当于:

    1
    length :: [a] -> Int

    就是这样。

    因为现在可以将类型变量绑定到某个范围,所以可以将范围设置为其他而不是像你的第一个例子那样的顶级水平("普遍量化"),其中类型变量仅在数据结构中可见。这允许对于隐藏类型("存在类型")。或者我们可以任意绑定的嵌套("秩n类型")。

    要深入理解类型系统,您需要学习一些行话。那是计算机科学的本质。但是,像上面这样的简单用法应该是能够直观地掌握,通过与"let"在价值层面上的类比。一很好的介绍是Launchbury和Peyton Jones。


    They're densely packed with assumptions that I've read the latest in whatever branch of discrete math, category theory or abstract algebra is popular this week. (If I never read the words"consult the paper whatever for details of implementation" again, it will be too soon.)

    呃,那么简单的一阶逻辑呢?forall在通用量化方面相当明显,在这种情况下,术语存在主义也更为合理,不过如果有exists关键字就不那么尴尬了。量化是有效通用的还是存在主义的,取决于量词相对于变量在函数箭头的哪一侧使用的位置的位置,而这一切都有点令人困惑。

    因此,如果这不起作用,或者你只是不喜欢符号逻辑,从一个更为函数式编程的角度来看,你可以把类型变量看作是(隐式)函数的类型参数。从这个意义上讲,采用类型参数的函数通常是出于任何原因使用大写lambda编写的,这里我将把它写成/\

    因此,考虑id函数:

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

    我们可以将其重写为lambda,将"类型参数"移出类型签名并添加内联类型批注:

    1
    id = /\a -> (\x -> x) :: a -> a

    以下是对const所做的相同的事情:

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

    所以你的bar函数可能是这样的:

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

    注意,作为参数提供给bar的函数的类型取决于bar的类型参数。考虑一下,如果你有这样的东西:

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

    在这里,bar2将该函数应用于Char类型的某个对象,因此给bar2Char以外的任何类型参数都将导致类型错误。

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

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

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

    因此,当您在类型签名中看到forall时,只需将其视为类型签名的lambda表达式。就像正则lambda一样,forall的作用域尽可能地向右扩展,直到括起来的括号,就像正则lambda中绑定的变量一样,由forall绑定的类型变量只在量化表达式的作用域内。

    post-scriptum:也许你会想——既然我们正在考虑使用类型参数的函数,为什么我们不能对这些参数做一些比将它们放入类型签名更有趣的事情呢?答案是我们可以!

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

    1
    Either = /\a b -> ...

    但是我们需要一个全新的符号,因为这种类型的编写方式,如Either a b,已经暗示"将函数Either应用于这些参数"。

    另一方面,在类型参数上排序"模式匹配"的函数(为不同类型返回不同的值)是一个类型类的方法。上面对我的/\语法的一个稍微扩展表明如下:

    1
    2
    3
    4
    5
    6
    7
    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的实际语法…

    一个"模式匹配"其类型参数并返回任意现有类型的函数是一个类型族或函数依赖关系——在前一种情况下,它甚至看起来非常像一个函数定义。


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

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

    通用量化好的。

    普遍量化的类型是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来表示"存在"类型,如下所示?好的。

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

    这可能会令人困惑,但实际上它描述的是数据构造函数SB的类型:好的。

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

    构造后,可以将ShowBox类型的值视为包含两个内容。它是一个EDOCX1型〔19〕和一个EDOCX1型〔19〕的值。换句话说,它是一个存在量化类型的值。如果haskell支持这个符号,那么ShowBox实际上可以写成exists s. Show s => s。好的。

    runST和朋友好的。

    鉴于此,这些有什么不同?好的。

    1
    2
    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类型的含义:好的。

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

    因此,无论我们以a的形式给出什么类型,runST都必须能够产生a类型的值。为此,它需要一个forall s. ST s a型的参数,它在发动机罩下只是forall s. s -> (a, s)型的函数。然后,该函数必须能够产生(a, s)类型的值,无论runST的实现决定以s的形式给出什么类型的值。好的。

    好吧,那又怎么样?其好处是,这对runST的调用方造成了限制,因为a类型根本不涉及s类型。例如,不能传递类型为ST s ▼显示的值。这在实践中意味着,runST的实现可以自由地进行值为s的突变。类型系统保证了这种突变是runST的局部实现。好的。

    runST的类型是rank-2多态类型的一个例子,因为它的参数类型包含forall量词。上面的foo类型也属于2级。一个普通的多态类型,如bar的类型,是rank-1,但是如果参数的类型需要多态的话,它就变成rank-2,有自己的forall量词。如果一个函数接受秩2参数,那么它的类型就是秩3,依此类推。一般来说,采用n级多态参数的类型具有n + 1级。好的。好啊。


    这个关键字有不同用法的原因是它实际上至少用于两个不同类型的系统扩展:更高级别的类型和存在主义。

    最好是单独阅读和理解这两件事,而不是同时试图解释为什么"forall"是两件事的适当语法。


    Can anybody completely explain the forall keyword in clear, plain English (or, if it exists somewhere, point to such a clear explanation which I've missed) that doesn't assume I'm a mathematician steeped in the jargon?

    我将试着解释一下forall在haskell及其类型系统中的意义和应用。

    但在你理解之前,我想告诉你一个非常容易和友好的谈话,由鲁纳比亚纳森题为"约束解放,自由约束"。尽管没有提到forall,但讨论中充满了来自现实世界用例的示例以及scala中支持此语句的示例。我将尝试解释下面的forall透视图。

    1
                    CONSTRAINTS LIBERATE, LIBERTIES CONSTRAIN

    要理解并相信这一说法,继续下面的解释是非常重要的,所以我敦促你观看这场谈话(至少是其中的一部分)。

    现在一个非常常见的例子,显示了haskell类型系统的表现性,这是类型签名:

    foo :: a -> a

    据说,对于这种类型的签名,只有一个函数可以满足这种类型,即identity函数或更广为人知的id函数。

    在我学习haskell的最初阶段,我总是想知道以下功能:

    1
    2
    3
    foo 5 = 6

    foo True = False

    他们都满足上面的类型签名,那么为什么哈斯克尔的人声称只有id满足类型签名呢?

    这是因为类型签名中隐藏了一个隐式forall。实际类型为:

    1
    id :: forall a. a -> a

    那么,现在让我们回到这句话:约束解放,自由约束

    将其转换为类型系统时,此语句变为:

    类型级别的约束在术语级别变为自由

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

    让我们试着证明第一句话:

    类型级别的约束。

    所以对我们的类型签名进行约束

    1
    foo :: (Num a) => a -> a

    成为术语层面的自由给我们自由或灵活性来写所有这些

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

    同样可以通过用任何其他类型类等约束a来观察到。

    所以现在这种类型的签名:foo :: (Num a) => a -> a翻译为:

    1
    ?a , st a -> a, ?a ∈ Num

    这就是所谓的存在主义量化,也就是说存在一些a的实例,对于这些实例,当函数输入a类型的东西时,会返回相同类型的东西,而这些实例都属于数字集。

    因此,我们可以看到添加一个约束(a应该属于一组数字),将术语级别解放为具有多个可能的实现。

    下面是第二个声明,实际载有对forall的解释:

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

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

    1
    foo :: forall a. a -> a

    现在这意味着:

    1
    ?a , a -> a

    这意味着该类型签名的实现应该在所有情况下都是a -> a

    所以现在这开始限制我们在术语层面上。我们不能再写了

    1
    foo 5 = 7

    因为如果我们把a放在Bool中,这个实现就不满足了。a可以是Char[Char]或自定义数据类型。在任何情况下,它都应该返回类似类型的内容。在类型层次上的自由度是所谓的普遍量化,唯一能满足这一点的函数是

    1
    foo a = a

    通常称为identity函数

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


    存在主义是怎样存在的?

    With Existential-Quantification, foralls in data definitions mean
    that, the value contained can be of any suitable type, not
    that it must be of all suitable types.
    -- yachiru's answer

    关于data中的forall定义为何与(exists a. a)定义(伪haskell)同构的解释,可以在维基书籍的"haskell/现有量化类型"中找到。

    以下是一个简短的逐字摘要:

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

    当模式匹配/解构MkT x时,x的类型是什么?

    1
    foo (MkT x) = ... -- -- what is the type of x?

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

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

    因此,以下是同构的:

    1
    2
    data T = forall a. MkT a -- an existential datatype
    data T = MkT (exists a. a) -- (pseudo-Haskell)

    一切意味着一切

    我对这一切的简单解释是,"forall的意思是‘为所有人’"。一个重要的区别是forall对定义和功能应用的影响。

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

    如果所定义的对象是一个多态值,那么它意味着该值必须对所有合适的a都有效,这是非常严格的。

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

    如果一个forall在函数参数的类型(即Rank2Type中)内,则意味着应用的参数必须是真正的多态的,以符合forall的思想,即定义是多态的。在这种情况下,可以在函数定义中多次选择参数类型("并由函数的实现选择",如诺曼所指出的那样)。

    因此,现有的data定义允许任何a的原因是数据构造函数是一个多态函数:

    1
    MkT :: forall a. a -> T

    市场类型:a -> *

    这意味着任何a都可以应用于该功能。例如,与多态值相反:

    1
    valueT :: forall a. [a]

    价值类型:a

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

    1
    [] :: [t]

    差异

    虽然forall的含义在ExistentialQuantificationRankNType中是一致的,但存在主义者有区别,因为data构造器可以用于模式匹配。如GHC用户指南中所述:

    When pattern matching, each pattern match introduces a new, distinct, type for each existential type variable. These types cannot be unified with any other type, nor can they escape from the scope of the pattern match.