输入奥秘。为什么这段代码会编译?

时间:2016-01-13 20:20:21

标签: haskell

此代码无法编译:

default ()

f :: RealFloat a => a
f = 1.0

g :: RealFloat a => a
g = 1.0

h :: Bool
h = f < g --Error. Ambiguous.

这是预期的,因为它含糊不清。这两种可能性是FloatDouble,编译器不知道选择哪个<

但是,此代码 编译:

default ()

f :: RealFloat a => a
f = 1.0

g :: RealFloat a => a
g = 1.0

h :: RealFloat a => a
h = f + g --Why does this compile?

为什么呢?为什么Haskell在这里与上面的示例中的方式类似,关于哪个+要选择(FloatDouble)?

3 个答案:

答案 0 :(得分:24)

在您的第二个示例h 中,也具有多态类型。因此+使用的类型不明确;它还没有被选中。

h 使用的上下文将决定选择哪种类型+(不同的使用网站可以做出不同的选择)。 h的用户可以要求其提供他们喜欢的任何RealFloat类型; fg也可以提供任何RealFloat类型,因此h只会询问他们用户所要求的类型。

答案 1 :(得分:11)

对于h = f < g :: Bool,类型Bool不包含多态a变量。要实际计算Bool结果,需要实例化a,结果Bool值可能取决于a的选择(通过RealFloat实例),所以GHC拒绝编译,而不是任意选择。

使用h = f + ga参数属于结果类型,因此不存在歧义。 a的选择尚未完成,我们仍然可以根据需要实例化a(或者更准确地说,我们重新概括了f + g的类型)。

答案 2 :(得分:5)

为了准确理解多态的含义,我发现使用显式类型参数来思考函数式语言会很方便 - 无论是理论上的,如系统F,还是现实世界的,如Agda,Idris,Coq等。

在这些语言中, types 作为函数参数传递,通常是值。如果我们有多态函数

f :: forall a. T a

这实际上需要类型作为第一个参数,如下所示:

f Int :: T Int
f Char :: T Char
f String :: T String
...

注意结果类型中的a如何实例化为type参数。

添加类型类约束,我们有

f :: RealFloat a => a
f = 1.0

可以看作是一个期望的函数:1)类型参数a,2)证明所选类型是RealFloat(例如类型类词典)。提供此选项后,将返回所选类型a的结果。更迂腐的定义可能是

f :: forall a. RealFloat a => a
f = \\a -> \\proof ->  ... -- use proof to generate 1.0 :: a

其中\\用作类型级lambda,用于前面描述的其他参数。然后可以如下呼叫:

-- pseudo syntax
f Double double_is_a_RealFloat_proof

将返回1.0 :: Double

现在,如果我们编写发布的代码会发生什么?

h :: RealFloat a => a
h = f + g

好吧,现在fg期望类型参数,以及h,因为这三个都是多态值。在类型推断期间,编译器会添加一些额外的参数,如下所示:

h :: forall a. RealFloat a => a
h = \\a -> \\proof -> (f a proof) + (g a proof)

(从技术上讲,即使是+,也是多态的,还有其他的论据,但为了便于阅读,我们把它放在地毯下......)

请注意,现在很清楚f应该生成什么类型​​:它是a,与h生成的类型相同。换句话说,h询问其调用者需要哪种类型,并将相同类型转发给f。同上g

相比之下,

h :: Bool
h = f < g

h中没有多态,但fg仍然是多态的。在类型推断期间,编译器到达

h = (f a? proof?) < (g a? proof?)

并且必须凭空创造a?proof?,因为h并没有要求他们给来电者。因此模棱两可的错误。

最后,请注意,可以在类型推断期间查看GHC添加的其他类型参数。为此,只需转储GHC Core中间语言即可。使用-ddump-simpl GHC标志。在尚未发布的GHC 8.x中,有传言称我们甚至可以在我们想要的时候在代码中指定显式类型参数,并让编译器像往常一样推断它们。听起来很有趣!