我在围绕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
在每个角色中扮演的角色是什么?
答案 0 :(得分:4)
(另一个角度,没有调用"隐式字典传递"类型类的实现):
Haskell中的 forall a.
表示"对于每种类型a
"。 1 它引入了一个类型变量,并声明了类型表达式的其余部分必须是有效的为a
做出的选择。
你通常不会在基本的Haskell中看到它(没有在GHC中启用任何扩展),因为它没有必要;你只需在类型签名中使用类型变量,GHC会自动假设forall
在表达式的开头引入了这些变量。
例如:
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)
对列表,无论如何我们希望选择a
和b
。
forall
主要用于扩展,因为如果你不明确地写它们,你可以引入范围其他的类型变量而不是GHC假设的默认类型变量。
现在,constraints => type
语法可以大致读作"这些约束意味着这种类型"或者#34;如果这些约束成立,你可以使用这种类型"。它一直在使用,即使在没有扩展的vanilla Haskell中也是如此,因此了解它的含义及其工作方式非常重要,而不仅仅是复制,粘贴和希望。
=>
箭头允许我们在类型表达式的其余部分中对变量说明一组约束;它让我们限制了引入类型变量的选择。您应首先忽略=>
箭头左侧的所有内容,然后单独阅读右侧部分。这为你提供了'#34;形状"这种类型。 =>
箭头左侧的内容告诉您可以使用其他类型的类型。
一个例子:
(+) :: Num a => a -> a -> a
这意味着(+)
与a -> a -> a
,等更简单的类型完全相同,除了 Num a =>
告诉我们我们& #39;不能自由选择任何类型a
。当我们知道它是a
类型类的成员时,我们只能选择Num
的类型(另一种更精确的说法方式" a
是其成员Num
是"约束Num a
包含")。
请注意,GHC仍然认为这里引入了类型变量forall a
的隐式a
,所以看起来确实如此:
(+) :: forall a. Num a => a -> a -> a
在这种情况下,一旦您知道forall a.
和Num a =>
的含义,您就可以轻松地将其作为英语句子轻松阅读:"对于每种类型a
,提供{{ 1}}持有,加上类型为Num a
"。
1 如果你完全熟悉形式逻辑,它只是一种ASCII友好的写作方式∀a,一个普遍量化的变量&#34 ;
答案 1 :(得分:2)
当forall
问题似乎已解决时,我会尝试解释一下=>
。 =>
左侧的内容是参数,非常类似->
左侧的参数。但是你不能手动应用这些参数,它们只能有特定的类型。
f :: Num a => a -> a
是一个带有两个参数的函数:
Num a
字典。a
。当您应用f
时,您只需提供a
即可。 GHC必须提供Num a
。如果它应用于特定的具体类型,如Int
,GHC知道Num Int
并可以在呼叫站点提供它。否则,它会检查某些外部上下文是否提供Num a
并使用该上下文。 Haskell的类型类系统的优点是它确保任何两个Num a
词典,无论它们是什么,都是相同的。所以字典的来源并不重要 - 它肯定是正确的。
我们谈论的很多这些事情并不完全是Haskell的一部分,因为它们是GHC通过转换为GHC核心,AKA系统FC来解释Haskell的方式的一部分,这是一个非常好的扩展 - 研究了F系统,AKA是Girard-Reynolds演算。系统FC是具有代数数据类型等的显式类型多态lambda演算,但没有类型推断,没有实例解析等。在GHC检查Haskell代码中的类型之后,它通过完全机械的过程将该代码转换为系统FC。它可以自信地做到这一点,因为类型检查器使用desugarer需要检测所有字典的所有信息“装饰”代码。如果你有一个看起来像
的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线,除了它需要一个类型变量。 @
表示将类型应用于需要一个的函数。 +
实际上只是一个记录选择器。它从它传递的字典中选择正确的字段。
答案 2 :(得分:1)
如果我们添加隐含的括号,我认为这会有所帮助:
(+) :: ∀ a . ( Num a => (a -> (a -> a)) )
id :: ∀ a . ( a -> a )
∀
始终与.
一起使用。它基本上是特殊的语法,意思是“∀
和.
之间的任何内容都是我要引入以下范围的类型变量”†
=>
表示Idris称之为隐式函数:Num a
是instance Num a
的字典,无论何时你都需要这样的字典添加数字。但是a
是否是以前由某些∀
或固定类型引入的类型变量并不重要。你也可以
(+) :: Num Int => Int -> Int -> Int
这只是多余的,因为编译器知道 Int
是Num
实例,因此会自动(隐式!)选择正确的字典。
实际上,∀
和=>
之间没有特别的关系,它们恰好经常在一起使用。
† 实际上这是一个类型级别的lambda 。类型表达式∀ a . b
的行为类似于值级别表达式\a -> b
。