什么是单态性限制?
Haskell Wiki声明的单态性限制为:
Haskell类型推论中的违反直觉的规则。如果您忘记提供类型签名,则有时该规则会使用“类型默认值”规则将自由类型变量填充为特定类型。
这意味着在某些情况下,如果您的类型不明确(即多态),则编译器将选择将该类型实例化为不明确的内容。
我如何解决它?
首先,您始终可以显式提供类型签名,这将避免触发限制:
plus :: Num a => a -> a -> a
plus = (+)
Prelude> plus 1.0 1
2.0
另外,如果要定义一个函数,则可以避免使用无 点样式,例如,编写:
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
具有typeNum a => a -> a -> a
的多态类型。而且碰巧上面的定义属于我稍后会解释的规则,因此他决定通过默认类型变量使类型单态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
同样,模数卫队。
请注意,变量是pattern,因此绑定:
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)] =读取t
回想一下这reads
是一个标准函数,其类型由签名给出
读取::(读取a)=>字符串-> [(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
和,因此我们将拥有plus :: Double -> Double -> Double
和x :: Double
。
违约
如前所述,存在一些默认规则,如报告的4.3.4节所述,推理者可以采用这些默认规则,并将其替换为单态类型。只要类型不明确,就会发生这种情况。
例如在表达式中:
let x = read "<something>" in show x
这里的表达式是模棱两可的,因为show
and的类型read
是:
show :: Show a => a -> String
read :: Read a => String -> a
因此x
具有类型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
约束导致默认值()
几乎没有用。
有用的链接
有很多的资源和对单态限制的讨论。
以下是一些我认为有用的链接,这些链接可以帮助您理解或深入了解该主题: