什么是单态性限制?


76

我对haskell编译器有时会推断出比我期望的少多态的类型感到困惑,例如,在使用无点定义时。

似乎问题出在“单态限制”,在较早版本的编译器上默认启用。

考虑以下haskell程序:

{-# LANGUAGE MonomorphismRestriction #-}

import Data.List(sortBy)

plus = (+)
plus' x = (+ x)

sort = sortBy compare

main = do
  print $ plus' 1.0 2.0
  print $ plus 1.0 2.0
  print $ sort [3, 1, 2]

如果我使用它进行编译,则ghc不会得到错误,并且可执行文件的输出为:

3.0
3.0
[1,2,3]

如果我将main主体更改为:

main = do
  print $ plus' 1.0 2.0
  print $ plus (1 :: Int) 2
  print $ sort [3, 1, 2]

我没有编译时错误,输出变为:

3.0
3
[1,2,3]

如预期的那样。但是,如果我尝试将其更改为:

main = do
  print $ plus' 1.0 2.0
  print $ plus (1 :: Int) 2
  print $ plus 1.0 2.0
  print $ sort [3, 1, 2]

我收到类型错误:

test.hs:13:16:
    No instance for (Fractional Int) arising from the literal ‘1.0’
    In the first argument of ‘plus’, namely ‘1.0’
    In the second argument of ‘($)’, namely ‘plus 1.0 2.0’
    In a stmt of a 'do' block: print $ plus 1.0 2.0

尝试sort使用不同类型的电话两次时,也会发生相同的情况:

main = do
  print $ plus' 1.0 2.0
  print $ plus 1.0 2.0
  print $ sort [3, 1, 2]
  print $ sort "cba"

产生以下错误:

test.hs:14:17:
    No instance for (Num Char) arising from the literal ‘3’
    In the expression: 3
    In the first argument of ‘sort’, namely ‘[3, 1, 2]’
    In the second argument of ‘($)’, namely ‘sort [3, 1, 2]’
  • 为什么ghc突然认为这plus不是多态的并且需要一个Int参数?唯一的参考Int是在的应用plus,当定义显然是多态的时候怎么办?
  • 为什么ghc突然认为sort需要Num Char实例?

此外,如果我尝试将函数定义放入其自己的模块中,如下所示:

{-# LANGUAGE MonomorphismRestriction #-}

module TestMono where

import Data.List(sortBy)

plus = (+)
plus' x = (+ x)

sort = sortBy compare

编译时出现以下错误:

TestMono.hs:10:15:
    No instance for (Ord a0) arising from a use of ‘compare’
    The type variable ‘a0’ is ambiguous
    Relevant bindings include
      sort :: [a0] -> [a0] (bound at TestMono.hs:10:1)
    Note: there are several potential instances:
      instance Integral a => Ord (GHC.Real.Ratio a)
        -- Defined in ‘GHC.Real’
      instance Ord () -- Defined in ‘GHC.Classes’
      instance (Ord a, Ord b) => Ord (a, b) -- Defined in ‘GHC.Classes’
      ...plus 23 others
    In the first argument of ‘sortBy’, namely ‘compare’
    In the expression: sortBy compare
    In an equation for ‘sort’: sort = sortBy compare
  • 为什么不能ghc将多态类型Ord a => [a] -> [a]用于sort
  • 又为何ghc对待plusplus'不同?plus应该具有多态类型Num a => a -> a -> a,但我真的看不出它与的类型有什么不同sort,但只会sort引发错误。

最后一件事:如果我评论sort该文件的定义编译。但是,如果我尝试将其加载到ghci并检查类型,我会得到:

*TestMono> :t plus
plus :: Integer -> Integer -> Integer
*TestMono> :t plus'
plus' :: Num a => a -> a -> a

为什么类型不是plus多态的?


正如元问题中讨论的,这是关于Haskell中单态限制的规范问题


为什么突然宣布公共服务?另外,我认为应该在您的答案中更突出地建议“关闭”。
dfeuer 2015年

2
@dfeuer突然?这个meta问题是在4个月前提出的。我两周前发布了以下答案的草稿。我在很久以前的聊天中也提到了这两个事实。对我来说,这不是“突然的”。明天我将看到如何突出显示最重要的信息。
Bakuriu

1
啊,我错过了meta参考。
dfeuer

Answers:


101

什么是单态性限制?

Haskell Wiki声明的单态性限制为:

Haskell类型推论中的违反直觉的规则。如果您忘记提供类型签名,则有时该规则会使用“类型默认值”规则将自由类型变量填充为特定类型。

这意味着在某些情况下,如果您的类型不明确(即多态),则编译器将选择将该类型实例化为不明确的内容。

我如何解决它?

首先,您始终可以显式提供类型签名,这将避免触发限制:

plus :: Num a => a -> a -> a
plus = (+)    -- Okay!

-- Runs as:
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的多态类型。而且碰巧上面的定义属于我稍后会解释的规则,因此他决定通过默认类型变量使类型单态aInteger如我们所见,默认值是。

请注意,如果您尝试使用编译以上代码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

我们预计所有这些功能,以同样的方式表现,并具有相同的类型,即类型showShow 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

所以f2f4不要编译。而且,当尝试在GHCi中定义这些功能时,我们不会出错,但是f2and的类型f4() -> String

单态性限制是构成单态类型f2f4要求的一种,而不同的行为之间ghcghci是由于不同的默认规则所致 。

什么时候发生?

根据报告的定义,在Haskell中,有两种不同的绑定类型。函数绑定和模式绑定。函数绑定就是函数的定义:

f x = x + 1

请注意,它们的语法为:

<identifier> arg1 arg2 ... argn = expr

模态警卫和where声明。但是它们并不重要。

必须至少有一个论点的地方

模式绑定是以​​下形式的声明:

<pattern> = expr

同样,模数卫队。

请注意,变量是pattern,因此绑定:

plus = (+)

模式绑定。它将模式plus(变量)绑定到表达式(+)

当模式绑定仅包含变量名时,称为 简单模式绑定。

单态性限制适用于简单的模式绑定!

好吧,我们应该正式地说:

声明组是相互依赖的绑定的最小集合。

报告第4.5.1节。

然后(报告的第4.5.5节):

给定的声明组不受限制,且仅在以下情况下:

  1. 组中的每个变量都由函数绑定(例如f x = x)或简单模式绑定(例如plus = (+)4.4.3.2节)绑定,并且

  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通过调用Integerx,它将与统一类型变量aInteger因此类型plus变为:

Integer -> Integer -> Integer

但是,当它进行类型检查时y,它会看到plus 应用于Double参数,并且类型不匹配。

请注意,您仍然可以使用plus而不会出现错误:

plus = (+)
x = plus 1.0 2

在这种情况下,plus首先推断的类型为,Num a => a -> a -> a 但随后x1.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 ⇒ as type ∀ a. Read a ⇒ String。后者是无效类型,因为它本质上是模棱两可的。无法确定要使用哪种重载s,也不能通过为添加类型签名来解决s。因此,当使用非简单模式绑定时(第4.4.3.2节),无论是否提供类型签名,推断出的类型在其受约束的类型变量中总是单态的。在这种情况下,n和和s在中都是单态的a

好吧,我相信这个例子是不言而喻的。在某些情况下,不应用规则会导致类型不明确。

如果按照上面的建议禁用扩展名,则在尝试编译上述声明时出现类型错误。但是,这并不是真正的问题:您已经知道,在使用read时必须以某种方式告诉编译器应尝试解析的类型...

第二条规则

  1. 当完成整个模块的类型推断时,所有残留的单态类型变量都被视为模棱两可,并使用默认规则将其解析为特定类型(第4.3.4节)。

这意味着。如果您有通常的定义:

plus = (+)

由于上述规则1 Num a => a -> a -> aa它的 类型将为其中单态类型变量。一旦推断出整个模块,编译器将简单地选择一种类型,该类型将a 根据默认规则替换该类型。

最终结果是:plus :: Integer -> Integer -> Integer

请注意,这是推断整个模块之后完成的。

这意味着如果您具有以下声明:

plus = (+)

x = plus 1.0 2.0

一个模块里面,之前键入违约的类型plus将是: Fractional a => a -> a -> a(见为什么会这样,第1条)。此时,遵循默认规则,a将被替换为Double 和,因此我们将拥有plus :: Double -> Double -> Doublex :: Double

违约

如前所述,存在一些默认规则,如报告的4.3.4节所述,推理者可以采用这些默认规则,并将其替换为单态类型。只要类型不明确,就会发生这种情况。

例如在表达式中:

let x = read "<something>" in show x

这里的表达式是模棱两可的,因为showand的类型read是:

show :: Show a => a -> String
read :: Read a => String -> a

因此x具有类型Read a => a。但这种约束是由很多类型的满足: IntDouble()例如。选择哪一个?没有什么可以告诉我们的。

在这种情况下,我们可以通过告诉编译器所需的类型并添加类型签名来解决歧义:

let x = read "<something>" :: Int in show x

现在的问题是:由于Haskell使用Num类型类来处理数字,因此在许多情况下,数字表达式包含歧义。

考虑:

show 1

结果应该是什么?

和以前一样,1有类型,Num a => a并且可以使用许多类型的数字。选择哪一个?

几乎每次我们使用数字时都会出现编译器错误不是一件好事,因此引入了默认规则。可以使用default声明来控制规则。通过指定,default (T1, T2, T3)我们可以更改推断者默认不同类型的方式。

在以下情况下,不明确的类型变量v是默认值:

  • v只出现在那种约束上C vC一类(即,如果它出现在: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,而且必须在OrdShow或其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约束导致默认值()几乎没有用。

有用的链接

很多的资源和对单态限制的讨论。

以下是一些我认为有用的链接,这些链接可以帮助您理解或深入了解该主题:

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.