繁体   English   中英

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

[英]Why do GHC and GHCI differ on type inference?

我注意到,在进行codegolf挑战时 ,默认情况下,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 -> ta -> Bool ,即使明确地使用-XMonomorphismRestriction运行-XMonomorphismRestriction

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

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

委员会用自己的话说,委员会作出这一决定的背景是由Paul Hudak 等人撰写的文章“ 哈斯克尔的历史:与班级一起懒惰

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

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

现在考虑这个定义:

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

看起来len应该只计算一次,但它实际上可以计算两次。 为什么? 因为我们可以推断类型len :: (Num a) => a; 当与字典传递翻译相结合时, len成为每次出现len调用一次的函数,每个len都可以使用不同的类型。

[约翰]休斯坚决认为以这种方式静默复制计算是不可接受的。 他的论点是由他编写的一个程序推动的,这个程序比他预期的要快得多。 (这当然是一个非常简单的编译器,但是我们不愿意将性能差异视为依赖于编译器优化的那么大。)

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

单态限制显然是语言的疣。 它似乎通过引发意外或模糊的错误消息来咬住每个新的Haskell程序员。 关于替代品的讨论很多。

(18,重点补充。)请注意,约翰休斯是该文章的合着者。

我无法复制你的结果GHCi推断类型(Foldable t, Eq a) => a -> ta -> Bool甚至与-XMonomorphismRestriction (GHC 8.0.2)。

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

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

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

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

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

这与我们充满希望的假设不一致,即所有使用站点都需要单个字典,因此单态限制导致了一个错误,可以通过推断(!)的更通用类型来避免这种错误。 这当然是值得商榷的是,单态限制阻止更多的问题比它解决的,但考虑到它存在,我们肯定会想GHCI有同样的表现方式,对不对?

但GHCi是一名翻译。 您一次输入一个语句,而不是一次输入一个模块。 所以,当你键入(!) = elem并回车,GHCI已经明白这种说法,并产生绑定到一个值(!)与一些特定类型的现在 (它可以是一个未计算的thunk,但我们要知道它的类型是)。 随着单态的限制,我们无法推断(Foldable t, Eq a) => a -> ta -> Bool ,我们要挑选那些类型变量类型现在 ,从使用的网站没有信息来帮助我们捡东西明智的。 GHCi中的扩展默认规则(与GHC的另一个区别)默认为[]() ,因此得到(!) :: () -> [()] -> Bool 1 相当无用,并且您尝试从示例中使用一用途时出现类型错误。

当您没有编写显式类型签名时,单态限制所解决的问题在数值计算的情况下特别严重。 由于Haskell的数字文字被重载,您可以轻松编写完整的复杂计算,并使用起始数据,其最常见的类型是多态的,具有NumFloating或etc约束。 大多数内建数字类型都非常小,所以你很可能有你宁愿存储比计算多次值。 情景更有可能发生,更有可能成为问题。

但它也完全与数字类型一样,整个模块类型推理过程对于将类型变量默认为具体类型变量至关重要 (并且数字的小例子正是人们对Haskell的新手很可能在翻译中尝试)。 在GHCi中默认关闭单态限制之前,在Stack Overflow上有一个恒定的Haskell问题流,人们混淆了为什么他们不能在GHCi中划分他们可以编译代码中的数字,或类似的东西(基本上相反)你的问题)。 在编译的代码中,您可以大多只是按照您想要的方式编写代码而没有显式类型,并且完整模块类型推断会确定它是否应该将整数文字默认为Integer ,如果需要将它们添加到按length返回的内容,则为Int ,或Double ,如果他们需要添加的东西和别的东西这是其他地方的东西,等等等等划分在GHCI乘以一个简单的x = 2经常做的单态约束下的错误开启(因为它会挑Integer无论你以后想用x做什么,结果是你需要在一个快速简单的交互式解释器中添加更多的类型注释,甚至比最热心的显式打字员在生产编译代码中使用的更多。

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


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

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

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

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

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

链接

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM