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

时间:2017-05-22 23:45:51

标签: haskell ghc ghci

我注意到,在执行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

可以使用编译指示NoMonomorphismRestriction更改此内容。

但是,将此键入GHCI不会产生类型错误,而:t (!)会在此处显示(Foldable t, Eq a) => a -> t a -> Bool,即使明确地使用-XMonomorphismRestriction运行也是如此。

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

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

3 个答案:

答案 0 :(得分:5)

委员会自己的话,在Paul Hudak的文章“A History of Haskell: Being Lazy with Class”中给出了委员会作出这一决定的背景

  

早期阶段争议的主要来源是所谓的   “单态限制。”假设genericLength有这个   重载类型:

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

现在考虑这个定义:

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

看起来len只应计算一次,但它可以   实际上是两次计算。为什么?因为我们可以推断出类型   len :: (Num a) => a;因为字典传递而感到沮丧   翻译,len成为每个被调用一次的函数   len的出现,每个都可能以不同的类型使用。

     

[约翰]休斯坚决地说,默默地说这是不可接受的   以这种方式重复计算。他的论点是由他推动的   他编写的程序比预期的要慢得多。   (诚​​然,这是一个非常简单的编译器,但我们不愿意   使性能差异与依赖于编译器的性能差异一样大   的优化。)

     

经过多次辩论,委员会采纳了现在臭名昭着的   单态限制。简单地说,它说明了一个定义   看起来不像一个函数(即没有参数   任何超载的左侧都应该是单形的   类型变量。在此示例中,规则强制使用len   在它出现的同一类型,这解决了性能   问题。程序员可以为len提供显式类型签名   需要多态行为。

     

单态限制显然是语言的瑕疵。   似乎每个新的Haskell程序员都会产生一个问题   意外或模糊的错误信息。有很多   讨论替代方案。

(18,重点补充。)请注意,John Hughes是该文章的合着者。

答案 1 :(得分:3)

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

我看到的是,当我输入(!) = elem行时,它会推断出类型(!) :: () -> [()] -> Bool,这实际上是为什么你希望GHCi表现出来的完美例证"不同的"来自GHC,因为GHC正在使用单态限制。

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

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

GHC立即编译整个模块。因此,它可以同时看到使用网站和绑定网站 。因此,如果绑定的任何使用需要特定的具体类型,绑定将使用该类型的字典,只要所有其他使用站点与该类型兼容(即使它们实际上是这样),一切都会很好多态的,也可以使用其他类型)。即使引导正确类型的代码与许多其他调用的绑定广泛分离,这也有效;在类型检查/推理阶段,所有对事物类型的约束都通过统一有效地连接起来,所以当编译器在绑定站点选择类型时,它可以"参见"所有使用站点的要求(在同一模块内)。

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

这与我们充满希望的假设是一致的,即所有使用站点都需要单个字典,因此单态限制导致了一个错误,可以通过推断{{{{{{{ 1}}。毫无疑问,单态限制可以防止比解决更多的问题,但鉴于 ,我们当然希望GHCi的行为方式相同,对吗?

然而,GHCi是一名翻译。您一次输入一个语句,而不是一次输入一个模块。因此,当您键入(!)并按Enter键时,GHCi必须理解该语句并生成一个值以(!) = elem绑定到某个特定类型现在(它可能是未评估的thunk,但我们必须知道它的类型是什么)。由于单态限制我们无法推断(!),我们必须为那些类型变量现在选择一种类型,而没有来自use-sites的信息来帮助我们选择合理的东西。 GHCi中的扩展默认规则(与GHC的另一个区别)默认为(Foldable t, Eq a) => a -> t a -> Bool[],因此您获得() 1 。非常没用,并且在您的示例中尝试使用 时会出现类型错误。

当您不编写显式类型签名时,单态限制所解决的问题在数值计算的情况下特别严重。由于Haskell的数字文字被重载,您可以轻松地编写一个完整的复杂计算,包括起始数据,其最常见的类型是多态的,具有(!) :: () -> [()] -> BoolNum或等约束。大多数内置数字类型非常小,因此您很可能拥有多次 而非多次计算的值。情景更有可能发生,更有可能成为问题。

但它也正是数字类型,整个模块类型推断过程必要以一种完全可用的方式将类型变量默认为具体类型(和带有数字的小例子正是Haskell新手可能会在解释器中尝试的内容。在GHCi中默认关闭单态限制之前,在Stack Overflow上有一个Haskell问题源源不断,人们混淆了为什么他们无法将GHCi中的数字除以编译代码中的数字,或类似的东西(基本上是这里你的问题的反面)。在编译的代码中,您几乎可以按照您想要的方式编写代码而没有显式类型,并且完整模块类型推断会确定是否应将整数文字默认为Floating,或者Integer如果需要被添加到由Intlength返回的内容,如果它们需要添加到某个内容并乘以其他地方除以某些内容等等等等。在GHCi中,简单Double经常会出现单态限制下的错误(因为它会选择x = 2而不管你以后想要用Integer做什么),结果你需要添加更多在一个快速简单的交互式解释器中输入注释,甚至比最热心的显式打字员在生产编译代码中使用它。

因此,GHC是否应该使用单态限制肯定是有争议的;它旨在解决一个真正的问题,它也会导致其他一些 2 。但单变形限制对于解释者来说是一个可怕的想法。一次一行和一次模块类型推断之间的根本区别意味着即使它们都默认使用它,它们在实践中的表现也完全不同。没有单态限制的GHCi至少可以更有效地使用。

1 如果没有扩展的默认规则,你会得到一个关于模糊类型变量的错误,因为它没有任何来确定选择,而不是甚至是有些愚蠢的违约规则。

2 我发现它在实际开发中只是一种轻微的刺激,因为我为顶级绑定编写了类型签名。我发现这足以使单态限制很少适用,所以它对我没有帮助或阻碍。因此,我可能更愿意将它废弃,以便一切都能保持一致,特别是因为它似乎比学习者更多地咬我作为练习者。另一方面,在重要的情况下调试一个罕见的性能问题是很多比很少需要添加GHC恼人地推断的正确类型签名更难

答案 2 :(得分:0)

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

出于效率/性能原因,

MonomorphismRestriction是一个有用的默认值。具体而言,问题归结为:

  

类型类本质上引入了额外的函数参数 - 特别是实现有问题的实例的代码字典。在类型类多态模式绑定的情况下,你最终会变成看起来像模式绑定的东西 - 一个只能被评估一次的常量,进入真正的函数绑定,这是一些不会被记忆的东西。

Link