我注意到,在进行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 (!)
显示,即使使用-XMonomorphismRestriction
显式运行,它在这里也假定(Foldable t, Eq a) => a -> t a -> Bool
。
为什么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
提供一个显式类型签名,如果 需要多态性行为。单态限制显然是对语言的疣。 它似乎咬住了每一个新的Haskell程序员,产生了一个 意外或模糊的错误消息。有很多 讨论替代方案。
(18,着重号另加。 请注意,约翰·休斯是本文的合著者。
我无法复制您的结果,即即使使用-XMonomorphismRestriction
(GHC 8.0.2),GHCi 也会推断出(Foldable t, Eq a) => a -> t a -> Bool
类型。
我看到的是,当我输入行(!) = elem
它推断出类型(!) :: () -> [()] -> Bool
,这实际上完美地说明了为什么您希望 GHCi 的行为与 GHC "不同",因为 GHC 正在使用单态限制。
@Davislor 的答案中描述的单态限制旨在解决的问题是,您可以编写语法上看起来像是计算一次值的代码,将其绑定到名称,然后多次使用它,其中实际上绑定到名称的东西是对闭包的引用,等待类型类字典才能真正计算值。所有使用站点都会单独计算出他们需要传递的字典并再次计算值,即使所有使用站点实际上都选择了相同的字典(就像你写一个数字的函数,然后使用相同的参数从几个不同的地方调用它一样,你会得到相同的结果多次计算)。但是,如果用户将该绑定视为一个简单的值,那么这将是出乎意料的,并且所有使用站点极有可能都需要单个字典(因为用户期望引用从单个字典计算的单个值)。
单态限制强制 GHC 不推断仍需要字典的类型(对于没有语法参数的绑定)。所以现在字典在绑定站点选择一次,而不是在每次使用绑定时单独选择,并且值实际上只计算一次。但这只有在结合位点选择的字典是所有使用位点都会选择的正确字典时才有效。如果GHC在结合位点选择了错误的一个,那么所有的使用点都将是类型错误,即使它们都同意他们期望的类型(以及字典)。
GHC 一次编译整个模块。所以它可以同时看到使用位点和结合位点。因此,如果绑定的任何使用需要特定的具体类型,则绑定将使用该类型的字典,只要所有其他使用站点都与该类型兼容,一切都会很好(即使它们实际上是多态的并且也可以与其他类型一起使用)。即使确定正确类型的代码通过许多其他调用与绑定广泛分离,这也有效;在类型检查/推理阶段,对事物类型的所有约束都通过统一有效地连接起来,因此当编译器在绑定站点选择类型时,它可以"看到"来自所有使用站点的需求(在同一模块内)。
但是,如果使用站点与单个具体类型不一致,则会出现类型错误,如示例中所示。(!)
的一个使用站点要求将a
类型变量实例化为Char
,另一个需要也具有Num
实例的类型(Char
没有)。
这与我们希望的所有使用站点都需要一个字典的假设不一致,因此单态限制导致了一个错误,可以通过推断更通用的(!)
类型来避免。单态限制防止的问题多于解决的问题当然是有争议的,但考虑到它的存在,我们肯定希望GHCi以同样的方式运行,对吧?
然而,GHCi是一个口译员。一次输入一个语句的代码,而不是一次输入一个模块。因此,当您键入(!) = elem
并按回车键时,GHCi 必须理解该语句并生成一个值以立即绑定到具有某些特定类型的(!)
(它可能是一个未计算的 thunk,但我们必须知道它的类型是什么)。由于单态限制我们无法推断(Foldable t, Eq a) => a -> t a -> Bool
,我们现在必须为这些类型变量选择一个类型,没有来自使用站点的信息来帮助我们选择一些合理的东西。GHCi 中打开的扩展默认规则(与 GHC 的另一个区别)默认规则默认为[]
和()
,因此您得到(!) :: () -> [()] -> Bool
1。非常无用,并且尝试示例中的任一用途时都会遇到类型错误。
单态限制解决的问题在数字计算的情况下尤其严重,当您不编写显式类型签名时。由于 Haskell 的数字文字是重载的,因此您可以轻松地编写整个复杂的计算,包括起始数据,其最一般的类型是具有Num
或Floating
等约束的多态。大多数内置数值类型都非常小,因此您很可能拥有宁愿存储而不是多次计算的值。这种情况更有可能发生,也更有可能成为问题。
但也正是对于数值类型,整个模块类型推断过程对于以完全可用的方式将类型变量默认为具体类型至关重要(而带有数字的小示例正是 Haskell 新手可能会在解释器中尝试的)。在 GHCi 中默认关闭单态限制之前,在 Stack Overflow 上源源不断地出现 Haskell 问题,这些问题来自人们,他们困惑为什么他们不能在 GHCi 中除以他们可以在编译代码中的数字,或者类似的东西(基本上与你的问题相反这里)。在编译的代码中,您通常可以按照您想要的方式编写代码,没有显式类型,并且全模块类型推理确定它是否应该将整数文本默认为Integer
,或者Int
是否需要将它们添加到length
返回的内容中,或者Double
是否需要将它们添加到某物中并乘以其他地方除以某物的其他东西, 等等等等。在 GHCi 中,一个简单的x = 2
经常在打开单态限制的情况下做错事(因为无论你以后想用x
做什么,它都会选择Integer
),结果你需要在快速简便的交互式解释器中添加更多的类型注释,甚至比最热心的显式打字员在生产编译代码中使用的要多得多。
因此,GHC是否应该使用单态限制当然是有争议的;它旨在解决一个真正的问题,它只是也导致了其他一些问题2。但是单态限制对解释器来说是一个糟糕的主意。一次一行和一次模块类型推理之间的根本区别意味着,即使它们都默认使用它,它们在实践中的行为也完全不同。没有单态限制的GHCi至少更有用。
1如果没有扩展的默认规则,你反而会得到一个关于不明确的类型变量的错误,因为它没有任何可以确定的选择,即使是有点愚蠢的默认规则。
2我发现在实际开发中这只是一种轻微的刺激,因为我为顶级绑定编写了类型签名。我发现这足以使单态限制很少适用,因此它对我没有太大帮助或阻碍。因此,我可能宁愿它被废弃,以便一切一致,特别是因为它似乎比咬我作为从业者更频繁地咬学习者。另一方面,在重要的情况下调试罕见的性能问题比很少添加 GHC 烦人地无法推断的正确类型签名要困难得多。
NoMonomorphismRestriction
是GHCI 中有用的默认值,因为您不必在 repl 中写出那么多讨厌的类型签名。 GHCI 将尝试推断出它所能推断的最通用类型。
MonomorphismRestriction
是一个有用的默认值,否则出于效率/性能原因。具体来说,问题归结为以下事实:
TypeClasses本质上引入了额外的函数参数 - 特别是实现相关实例的代码字典。在typeclass多态模式绑定的情况下,你最终会把看起来像模式绑定的东西——一个只会被计算一次的常量——变成真正的函数绑定,一些不会被记住的东西。
链接