什么是单态限制?
Haskell wiki 所述的单态限制是:
Haskell 类型推理中的反直觉规则。如果您忘记提供类型签名,有时此规则会使用“类型默认”规则将自由类型变量填充为特定类型。
这意味着,在某些情况下,如果您的类型不明确(即多态),编译器会选择将该类型实例化为不明确的类型。
我如何解决它?
首先,您始终可以显式提供类型签名,这将避免触发限制:
plus :: Num a => a -> a -> a
plus = (+) -- Okay!
-- Runs as:
Prelude> plus 1.0 1
2.0
或者,如果你正在定义一个函数,你可以避免 point-free style,例如写:
plus x y = x + y
关闭它
可以简单地关闭限制,这样您就不必对代码做任何事情来修复它。该行为由两个扩展控制:
MonomorphismRestriction
将启用它(这是默认设置),同时
NoMonomorphismRestriction
将禁用它。
您可以将以下行放在文件的最顶部:
{-# LANGUAGE NoMonomorphismRestriction #-}
如果您使用 GHCi,您可以使用以下:set
命令启用扩展:
Prelude> :set -XNoMonomorphismRestriction
您还可以告诉ghc
从命令行启用扩展:
ghc ... -XNoMonomorphismRestriction
注意:与通过命令行选项选择扩展相比,您应该更喜欢第一个选项。
有关此扩展和其他扩展的说明,请参阅GHC 的页面。
完整的解释
我将尝试在下面总结您需要了解的所有内容,以了解什么是单态限制、引入它的原因以及它的行为方式。
一个例子
采用以下简单定义:
plus = (+)
你会认为能够+
用plus
. 特别是因为(+) :: Num a => a -> a -> a
您希望也有plus :: Num a => a -> a -> a
.
不幸的是,这种情况并非如此。例如,如果我们在 GHCi 中尝试以下操作:
Prelude> let plus = (+)
Prelude> plus 1.0 1
我们得到以下输出:
<interactive>:4:6:
No instance for (Fractional Integer) arising from the literal ‘1.0’
In the first argument of ‘plus’, namely ‘1.0’
In the expression: plus 1.0 1
In an equation for ‘it’: it = plus 1.0 1
您可能需要:set -XMonomorphismRestriction
更新的 GHCi 版本。
事实上,我们可以看到的类型plus
不是我们所期望的:
Prelude> :t plus
plus :: Integer -> Integer -> Integer
发生的事情是编译器看到plus
了 type Num a => a -> a -> a
,一种多态类型。此外,上面的定义恰好符合我稍后将解释的规则,因此他决定通过默认类型 variable来使类型单态a
。Integer
我们可以看到默认值。
请注意,如果您尝试使用编译上述代码,ghc
您将不会收到任何错误。这是由于如何ghci
处理(并且必须处理)交互式定义。基本上,在考虑以下内容之前,ghci
必须对输入的每条语句进行完整的类型检查;换句话说,就好像每个语句都在一个单独的
模块中。稍后我会解释为什么这很重要。
其他一些例子
考虑以下定义:
f1 x = show x
f2 = \x -> show x
f3 :: (Show a) => a -> String
f3 = \x -> show x
f4 = show
f5 :: (Show a) => a -> String
f5 = show
我们希望所有这些函数都以相同的方式运行并具有相同的类型,即show
:的类型Show a => a -> String
。
然而,在编译上述定义时,我们得到以下错误:
test.hs:3:12:
No instance for (Show a1) arising from a use of ‘show’
The type variable ‘a1’ is ambiguous
Relevant bindings include
x :: a1 (bound at blah.hs:3:7)
f2 :: a1 -> String (bound at blah.hs:3:1)
Note: there are several potential instances:
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
...plus 24 others
In the expression: show x
In the expression: \ x -> show x
In an equation for ‘f2’: f2 = \ x -> show x
test.hs:8:6:
No instance for (Show a0) arising from a use of ‘show’
The type variable ‘a0’ is ambiguous
Relevant bindings include f4 :: a0 -> String (bound at blah.hs:8:1)
Note: there are several potential instances:
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
...plus 24 others
In the expression: show
In an equation for ‘f4’: f4 = show
所以f2
不要f4
编译。此外,当尝试在 GHCi 中定义这些函数时,我们没有收到任何错误,但是f2
and的类型f4
是() -> String
!
单态限制是制造f2
和要求单态类型的原因,f4
不同的行为是由不同的
默认规则引起的。ghc
ghci
什么时候发生?
在 Haskell 中,正如报告所定义的,有两种不同类型的绑定。函数绑定和模式绑定。函数绑定只不过是函数的定义:
f x = x + 1
请注意,它们的语法是:
<identifier> arg1 arg2 ... argn = expr
模守卫和where
声明。但它们并不重要。
其中必须至少有一个论点。
模式绑定是以下形式的声明:
<pattern> = expr
再次,模守卫。
请注意,变量是模式,因此绑定:
plus = (+)
是一个模式绑定。plus
它将模式(变量)绑定到表达式(+)
。
当模式绑定只包含一个变量名时,它被称为
简单模式绑定。
单态限制适用于简单的模式绑定!
好吧,正式地说,我们应该这样说:
声明组是相互依赖的绑定的最小集合。
报告第 4.5.1 节。
然后(报告第 4.5.5 节):
一个给定的声明组是不受限制的当且仅当:
组中的每个变量都由函数绑定(例如f x = x
)或简单的模式绑定(例如plus = (+)
;第 4.4.3.2 节)绑定,并且
为由简单模式绑定绑定的组中的每个变量给出显式类型签名。(例如plus :: Num a => a -> a -> a; plus = (+)
)。
我添加的示例。
因此,受限声明组是一个组,其中要么存在
非简单模式绑定(例如(x:xs) = f something
或(f, g) = ((+), (-))
),要么存在一些没有类型签名的简单模式绑定(如plus = (+)
)。
单态性限制影响受限制的声明组。
大多数时候,您不定义相互递归函数,因此声明组只是一个绑定。
它有什么作用?
报告第 4.5.5 节中的两条规则描述了单态性限制。
第一条规则
通常 Hindley-Milner 对多态性的限制是,只有在环境中不自由出现的类型变量才能被泛化。此外,受限声明组的受限类型变量可能不会在该组的泛化步骤中泛化。
(回想一下,如果类型变量必须属于某个类型类,则它是受约束的;请参阅第 4.5.2 节。)
突出显示的部分是单态限制引入的内容。它说如果类型是多态的(即它包含一些类型变量)
并且该类型变量是受约束的(即它有一个类约束:例如,类型Num a => a -> a -> a
是多态的,因为它包含a
并且也a
受约束,因为它具有约束Num
它.)
那么它不能被概括。
简单来说,不泛化意味着函数的使用plus
可能会改变它的类型。
如果您有以下定义:
plus = (+)
x :: Integer
x = plus 1 2
y :: Double
y = plus 1.0 2
那么你会得到一个类型错误。因为当编译器在声明中看到它plus
被调用时,它将统一类型变量,因此类型变为:Integer
x
a
Integer
plus
Integer -> Integer -> Integer
但是,当它对 的定义进行类型检查时y
,它会看到它plus
应用于一个Double
参数,并且类型不匹配。
请注意,您仍然可以使用plus
而不会出现错误:
plus = (+)
x = plus 1.0 2
在这种情况下,plus
首先推断出的类型是,但随后在需要约束的情况下将Num a => a -> a -> a
其用于 的定义中
,会将其更改为。x
1.0
Fractional
Fractional a => a -> a -> a
基本原理
报告说:
需要规则 1 有两个原因,这两个原因都相当微妙。
规则 1 防止计算被意外重复。例如,genericLength
是一个标准函数(在 library 中Data.List
),其类型由下式给出
genericLength :: Num a => [b] -> a
现在考虑以下表达式:
let len = genericLength xs
in (len, len)
看起来好像len
应该只计算一次,但如果没有规则 1,它可能会计算两次,在两个不同的重载中的每一个处计算一次。
如果程序员确实希望重复计算,可以添加显式类型签名:
let len :: Num a => a
len = genericLength xs
in (len, len)
对于这一点,我相信来自wiki的示例更清晰。考虑函数:
f xs = (len, len)
where
len = genericLength xs
如果len
是多态的,则类型为f
:
f :: Num a, Num b => [c] -> (a, b)
所以元组的两个元素(len, len)
实际上可能是
不同的值!但这意味着genericLength
必须重复进行的计算才能获得两个不同的值。
这里的理由是:代码包含一个函数调用,但不引入这个规则可能会产生两个隐藏的函数调用,这是违反直觉的。
在单态限制下,类型f
变为:
f :: Num a => [b] -> (a, a)
这样就不需要多次执行计算。
规则 1 防止歧义。例如,考虑声明组
[(n,s)] = reads t
回想一下,这reads
是一个标准函数,其类型由签名给出
reads :: (Read a) => String -> [(a,String)]
如果没有规则 1,n
将被分配 type∀ a. Read a ⇒ a
和s
type ∀ a. Read a ⇒ String
。后者是无效类型,因为它本质上是模棱两可的。无法确定使用什么重载s
,也无法通过为 . 添加类型签名来解决此问题s
。因此,当使用非简单模式绑定时(第 4.4.3.2 节),无论是否提供类型签名,推断的类型在其受约束的类型变量中始终是单态的。在这种情况下,n
和s
都是单态的a
。
好吧,我相信这个例子是不言自明的。在某些情况下,不应用规则会导致类型歧义。
如果您按照上面的建议禁用扩展,则在尝试编译上述声明时会出现类型错误。然而这并不是一个真正的问题:你已经知道在使用时read
你必须以某种方式告诉编译器它应该尝试解析哪种类型......
第二条规则
- 当整个模块的类型推断完成时保留的任何单态类型变量都被认为是模棱两可的,并使用默认规则(第 4.3.4 节)解析为特定类型。
这意味着。如果您有通常的定义:
plus = (+)
由于上述规则 1,这将具有一个类型Num a => a -> a -> a
,其中a
是
单态类型变量。一旦推断出整个模块,编译器将a
根据默认规则简单地选择一个类型来替换它。
最后的结果是:plus :: Integer -> Integer -> Integer
。
请注意,这是在推断出整个模块之后完成的。
这意味着如果您有以下声明:
plus = (+)
x = plus 1.0 2.0
在模块内部,在类型默认之前,类型plus
将是:(
Fractional a => a -> a -> a
请参阅规则 1 了解为什么会发生这种情况)。此时,按照默认规则,a
将替换为Double
and,因此我们将拥有plus :: Double -> Double -> Double
and x :: Double
。
违约
如前所述,存在一些默认规则,如报告第 4.3.4 节所述,推理器可以采用这些规则,并将多态类型替换为单态类型。只要类型不明确,就会发生这种情况。
例如在表达式中:
let x = read "<something>" in show x
这里的表达式是模棱两可的,因为 和 的类型show
是read
:
show :: Show a => a -> String
read :: Read a => String -> a
所以x
has 类型Read a => a
。但是很多类型都满足了这个约束:
Int
或Double
例如()
。选择哪一个?没有什么可以告诉我们的。
在这种情况下,我们可以通过告诉编译器我们想要哪种类型,添加类型签名来解决歧义:
let x = read "<something>" :: Int in show x
现在的问题是:由于 Haskell 使用Num
类型类来处理数字,因此在很多情况下,数字表达式都包含歧义。
考虑:
show 1
结果应该是什么?
和以前一样,1
有类型Num a => a
,并且可以使用许多类型的数字。选择哪一个?
几乎每次使用数字时都会出现编译器错误并不是一件好事,因此引入了默认规则。可以使用default
声明来控制规则。通过指定default (T1, T2, T3)
,我们可以更改推理器默认不同类型的方式。
在以下情况下,模糊类型变量v
是可默认的:
v
仅出现在类型的约束C v
中C
是一个类(即,如果它出现在:Monad (m v)
那么它是不可默认的)。
- 这些类中至少有一个
Num
是Num
.
- 所有这些类都在 Prelude 或标准库中定义。
可默认类型变量被列表中的第一个类型替换,该类型default
是所有不明确变量的类的实例。
默认default
声明是default (Integer, Double)
.
例如:
plus = (+)
minus = (-)
x = plus 1.0 1
y = minus 2 1
推断的类型将是:
plus :: Fractional a => a -> a -> a
minus :: Num a => a -> a -> a
通过默认规则,它变成:
plus :: Double -> Double -> Double
minus :: Integer -> Integer -> Integer
请注意,这解释了为什么在问题的示例中只有sort
定义会引发错误。该类型Ord a => [a] -> [a]
不能默认,因为Ord
它不是数字类。
扩展违约
请注意, GHCi 带有扩展的默认规则(或此处为 GHC8),可以在文件中启用,也可以使用ExtendedDefaultRules
扩展名启用。
可默认类型变量不仅需要出现在所有类都是标准的约束中,而且必须至少有一个类位于
Eq
、或及其子类中。Ord
Show
Num
此外,默认default
声明是default ((), Integer, Double)
.
这可能会产生奇怪的结果。以问题为例:
Prelude> :set -XMonomorphismRestriction
Prelude> import Data.List(sortBy)
Prelude Data.List> let sort = sortBy compare
Prelude Data.List> :t sort
sort :: [()] -> [()]
在 ghci 中,我们没有收到类型错误,但Ord a
约束导致默认值()
几乎没有用。
有用的链接
关于单态性限制有很多资源和讨论。
以下是一些我认为有用的链接,可以帮助您理解或深入了解该主题: