为什么 GHC 和 GHCI 在类型推断上有所不同?

Why do GHC and GHCI differ on type inference?

我注意到,在执行 codegolf challenge 时,默认情况下,GHC 不会为变量推断出最通用的类​​型,当您尝试将其与两种不同类型一起使用时会导致类型错误。

例如:

(!) = elem
x = 'l' ! "hello" -- From its use here, GHC assumes (!) :: Char -> [Char] -> Bool
y = 5 ! [3..8] -- Fails because GHC expects these numbers to be of type Char, too

这可以使用 pragma NoMonomorphismRestriction.

更改

然而,将其输入 GHCI 不会产生类型错误,并且 :t (!) 显示这里假定 (Foldable t, Eq a) => a -> t a -> Bool,即使明确地 运行 和 -XMonomorphismRestriction 也是如此。

为什么 GHC 和 GHCI 在假定最通用的函数类型方面有所不同?

(另外,为什么默认启用它?它有什么帮助?)

NoMonomorphismRestriction 是 GHCI 中一个有用的默认值,因为您不必在 repl 中写出那么多讨厌的类型签名。 GHCI 将尝试推断它可以推断出的最一般类型。

MonomorphismRestriction 是一个有用的默认值,否则出于效率/性能原因。具体来说,问题归结为以下事实:

typeclasses essentially introduce additional function parameters -- specifically, the dictionary of code implementing the instances in question. In the case of typeclass polymorphic pattern bindings, you end up turning something that looked like a pattern binding -- a constant that would only ever be evaluated once, into what is really a function binding, something which will not be memoised.

Link

委员会做出此决定的背景在 Paul Hudak 等人的文章“A History of Haskell: Being Lazy with Class”中用设计师自己的话给出了

A major source of controversy in the early stages was the so-called “monomorphism restriction.” Suppose that genericLength has this overloaded type:

genericLength :: Num a => [b] -> a

Now consider this definition:

f xs = (len, len)`
  where
    len = genericLength xs

It looks as if len should be computed only once, but it can actually be computed twice. Why? Because we can infer the type len :: (Num a) => a; when desugared with the dictionary-passing translation, len becomes a function that is called once for each occurrence of len, each of which might used at a different type.

[John] Hughes argued strongly that it was unacceptable to silently duplicate computation in this way. His argument was motivated by a program he had written that ran exponentially slower than he expected. (This was admittedly with a very simple compiler, but we were reluctant to make performance differences as big as this dependent on compiler optimisations.)

Following much debate, the committee adopted the now-notorious monomorphism restriction. Stated briefly, it says that a definition that does not look like a function (i.e. has no arguments on the left-hand side) should be monomorphic in any overloaded type variables. In this example, the rule forces len to be used at the same type at both its occurrences, which solves the performance problem. The programmer can supply an explicit type signature for len if polymorphic behaviour is required.

The monomorphism restriction is manifestly a wart on the language. It seems to bite every new Haskell programmer by giving rise to an unexpected or obscure error message. There has been much discussion of alternatives.

(18,已添加重点。)请注意,John Hughes 是该文章的合著者。

即使使用 -XMonomorphismRestriction (GHC 8.0.2),我也无法复制 GHCi 推断类型 (Foldable t, Eq a) => a -> t a -> Bool 的结果。

我看到的是,当我输入行 (!) = elem 时,它会推断出类型 (!) :: () -> [()] -> Bool,这实际上完美地说明了为什么您希望 GHCi 的行为是 "differently" GHC,假设 GHC 使用单态限制。

@Davislor 的回答中描述的单态限制旨在解决的问题是,您可以编写在语法上看起来像是计算一次值的代码,将其绑定到一个名称,然后多次使用它,其中实际上,绑定到名称的东西是对等待类型 class 字典的闭包的引用,然后才能真正计算值。所有使用站点将分别计算出他们需要传递的字典并再次计算值,即使所有使用站点实际上都选择了相同的字典(就像您编写一个数字函数然后从几个不同的地方调用它一样使用相同的参数,你会得到多次计算的相同结果)。但是,如果用户 认为 将该绑定作为一个简单的值,那么这将是出乎意料的,并且极有可能所有使用站点都需要一个字典(因为用户期望一个引用从单个字典计算的单个值)。

单态性限制强制 GHC 不推断 仍然需要字典的类型(对于没有句法 参数的绑定)。所以现在字典在绑定站点选择一次,而不是在每次使用绑定时单独选择,并且值实际上只计算一次。但这只有在绑定站点选择的词典是所有使用站点都会选择的正确 的词典时才有效。如果 GHC 在绑定站点选择了错误的站点,那么所有使用站点都将是类型错误,即使他们都同意他们期望的类型(以及字典)。

GHC 一次编译整个模块。所以它可以同时看到使用站点和绑定站点。因此,如果绑定的任何使用需要特定的具体类型,绑定将使用该类型的字典,并且只要所有其他使用站点都与该类型兼容,一切都会很好(即使它们实际上是多态的并且也会曾与其他类型合作过)。即使确定正确类型的代码与许多其他调用的绑定相距甚远,这仍然有效;在类型 checking/inference 阶段通过统一有效地连接了对事物类型的所有约束,因此当编译器在绑定站点选择类型时,它可以 "see" 所有使用的要求-网站(在同一模块内)。

但是,如果使用站点并非都与单个具体类型一致,那么您会收到类型错误,如您的示例所示。 (!) 的一个使用点需要将 a 类型变量实例化为 Char,另一个需要一个也有 Num 实例的类型(Char 没有)。

这与我们希望所有使用站点都需要一个字典的假设不一致,因此单态性限制导致了一个错误,该错误本可以通过为 [= 推断更通用的类型来避免14=]。单态限制防止的问题多于它解决的问题当然值得商榷,但考虑到它存在,我们当然希望 GHCi 以相同的方式运行,对吗?

但是 GHCi 是一个解释器。您一次输入代码一个语句,而不是一次输入一个模块。因此,当您键入 (!) = elem 并按回车键时,GHCi 必须理解该语句并生成一个值以立即 以某种特定类型绑定到 (!)(它可以是一个未评估的 thunk,但我们必须知道它的类型是什么)。由于单态限制,我们无法推断 (Foldable t, Eq a) => a -> t a -> Bool,我们必须为这些类型变量选择一个类型 now,没有来自使用站点的信息来帮助我们选择一些合理的东西. GHCi 中启用的扩展默认规则(与 GHC 的另一个区别)默认为 [](),因此您得到 (!) :: () -> [()] -> Bool1。非常无用,您在尝试 任一 示例中的用途时都会遇到类型错误。

单态性限制解决的问题在您未编写显式类型签名的数值计算情况下尤为严重。由于 Haskell 的数字文字被重载,您可以轻松编写完整的复杂计算,包括起始数据,其最通用的类​​型是具有 NumFloating 等约束的多态。大多数内置数字类型都非常小,因此您很可能会 多次 存储而不是计算多次的值。这种情况更可能发生,也更可能成为问题。

但是对于数字类型来说,整个模块的类型推断过程必不可少以一种完全可用的方式将类型变量默认为具体类型(和带有数字的小例子正是 Haskell 的新手可能会在解释器中尝试的东西)。在 GHCi 中默认关闭单态性限制之前,Stack Overflow 上不断出现 Haskell 问题,人们困惑为什么他们不能在 GHCi 中除以编译代码或类似的东西(基本上与您的问题相反)。在编译后的代码中,您几乎可以按照自己的方式编写代码,而无需显式类型,全模块类型推断会确定是否应将整数文字默认为 Integer,或者如果需要,则为 Int添加到 lengthDouble 返回的内容,如果它们需要添加到某些内容并乘以其他内容,而其他内容在其他地方除以其他内容,等等。在 GHCi 中,一个简单的 x = 2在打开单态限制的情况下经常会出错(因为它会选择 Integer 而不管你以后想用 x 做什么),结果你需要添加更多的类型注释在一个快速简单的交互式解释器中,即使是最热心的显式打字员也会在生产编译代码中使用。

所以GHC是否应该使用单态限制当然是有争议的;它旨在解决一个实际问题,但它也会导致其他一些问题2。但是单态限制对于解释器来说是一个糟糕的想法。 line-at-a-time 和 module-at-a-time 类型推断之间的根本区别意味着,即使它们都默认使用它,但它们在实践中的行为却完全不同。没有单态限制的 GHCi 至少更有用。


1 如果没有扩展的默认规则,您反而会收到关于不明确类型变量的错误,因为它没有 anything确定一个选择,甚至是有点愚蠢的默认规则。

2 我发现它在实际开发中只是轻微的刺激,因为我为顶级绑定编写类型签名。我发现这足以使单态限制很少应用,所以它对我没有太大帮助或阻碍。因此,我可能宁愿将其废弃,以便一切都能始终如一地工作,尤其是当它对学习者的影响似乎比对我作为从业者的影响要大得多。另一方面,在重要的情况下调试一个罕见的性能问题 比很少添加 GHC 恼人地无法推断的正确类型签名要难得多