什么是单态限制?

What is the monomorphism restriction?

我对 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]’

此外,如果我尝试将函数定义放入它们自己的模块中,如:

{-# 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

最后一件事:如果我注释 sort 的定义,文件就会编译。然而 如果我尝试将它加载到 ghci 并检查我得到的类型:

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

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


这是 Haskell 中关于单态限制的规范问题 正如 [元问题](https://meta.whosebug.com/questions/294053/can-we-provide-a-canonical-questionanswer-for-haskells-monomorphism-restrictio) 中所讨论的那样。

什么是单态限制?

Haskell wiki 所述的 monomorphism restriction 是:

a counter-intuitive rule in Haskell type inference. If you forget to provide a type signature, sometimes this rule will fill the free type variables with specific types using "type defaulting" rules.

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

我该如何解决?

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

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

-- Runs as:
Prelude> plus 1.0 1
2.0

请注意,只有变量上的普通类型签名才算作此目的,表达式类型签名不算数。例如,这样写仍然会导致限制被触发:

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

或者,如果你正在定义一个函数,你可以避免 point-free style, 例如写:

plus x y = x + y

正在关闭

可以简单地关闭限制,这样你就不必做 任何你的代码来修复它。该行为由两个扩展控制: MonomorphismRestriction 将启用它(这是默认设置),同时 NoMonomorphismRestriction 将禁用它。

您可以将以下行放在文件的最顶部:

{-# LANGUAGE NoMonomorphismRestriction #-}

如果您使用的是 GHCi,则可以使用 :set 命令启用扩展:

Prelude> :set -XNoMonomorphismRestriction

您还可以告诉 ghc 从命令行启用扩展:

ghc ... -XNoMonomorphismRestriction

注意:您应该更喜欢第一个选项,而不是通过 command-line 选项选择扩展。

请参阅 GHC's page 了解此扩展和其他扩展的解释。

完整的解释

我将尝试在下面总结您需要了解的所有内容,以了解 单态限制是,为什么引入它以及它的行为方式。

一个例子

采用以下简单的定义:

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 具有类型 Num 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

所以 f2f4 不编译。此外,在尝试定义这些功能时 在 GHCi 中,我们得到 没有错误 ,但是 f2f4 的类型是 () -> String!

单态限制使得 f2f4 需要单态 类型,ghcghci 之间的不同行为是由于不同的 默认规则

什么时候发生?

在Haskell中定义为report, there are two distinct type of bindings。 函数绑定和模式绑定。函数绑定无非就是 一个函数的定义:

f x = x + 1

注意他们的语法是:

<identifier> arg1 arg2 ... argn = expr

模守卫和where声明。但它们并不重要。

其中必须有至少一个参数

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

<pattern> = expr

同样,模守卫。

注意变量是模式,所以绑定:

plus = (+)

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

当模式绑定只包含一个变量名时,它被称为 简单 模式绑定。

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

嗯,正式地我们应该这样说:

A declaration group is a minimal set of mutually dependent bindings.

report.

的第 4.5.1 节

然后(report 的第 4.5.5 节):

a given declaration group is unrestricted if and only if:

  1. every variable in the group is bound by a function binding (e.g. f x = x) or a simple pattern binding (e.g. plus = (+); Section 4.4.3.2 ), and

  2. an explicit type signature is given for every variable in the group that is bound by simple pattern binding. (e.g. plus :: Num a => a -> a -> a; plus = (+)).

我加的例子

所以 restricted 声明组是一个组,其中要么 non-simple 模式绑定(例如 (x:xs) = f something(f, g) = ((+), (-)))或 有一些没有类型签名的简单模式绑定(如 plus = (+))。

单态限制影响限制声明组。

大多数时候你没有定义相互递归函数,因此没有声明 组变成 a 绑定。

它有什么作用?

单态性限制由部分中的两个规则描述 report.

的4.5.5

第一条规则

The usual Hindley-Milner restriction on polymorphism is that only type variables that do not occur free in the environment may be generalized. In addition, the constrained type variables of a restricted declaration group may not be generalized in the generalization step for that group. (Recall that a type variable is constrained if it must belong to some type class; see Section 4.5.2 .)

突出显示的部分是单态限制引入的内容。 它说 if 类型是多态的(即它包含一些类型变量) 类型变量受到约束(即它具有 class 约束: 例如Num a => a -> a -> a 类型是多态的,因为它包含 a 和 也受到约束,因为 a 对其有约束 Num。) 那么不能一概而论

简单来说不概括意味着函数plus使用可能会改变它的类型。

如果你有定义:

plus = (+)

x :: Integer
x = plus 1 2

y :: Double
y = plus 1.0 2

然后你会得到一个类型错误。因为当编译器看到 plus 是 在 x 的声明中调用 Integer 它将统一类型 变量 aInteger 因此 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.

理由

报告说:

Rule 1 is required for two reasons, both of which are fairly subtle.

  • Rule 1 prevents computations from being unexpectedly repeated. For example, genericLength is a standard function (in library Data.List) whose type is given by

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

    Now consider the following expression:

      let len = genericLength xs
      in (len, len)
    

    It looks as if len should be computed only once, but without Rule 1 it might be computed twice, once at each of two different overloadings. If the programmer does actually wish the computation to be repeated, an explicit type signature may be added:

      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)

这样就不需要多次计算了

  • Rule 1 prevents ambiguity. For example, consider the declaration group

     [(n,s)] = reads t
    

    Recall that reads is a standard function whose type is given by the signature

     reads :: (Read a) => String -> [(a,String)]
    

    Without Rule 1, n would be assigned the type ∀ a. Read a ⇒ a and s the type ∀ a. Read a ⇒ String. The latter is an invalid type, because it is inherently ambiguous. It is not possible to determine at what overloading to use s, nor can this be solved by adding a type signature for s. Hence, when non-simple pattern bindings are used (Section 4.4.3.2 ), the types inferred are always monomorphic in their constrained type variables, irrespective of whether a type signature is provided. In this case, both n and s are monomorphic in a.

嗯,我相信这个例子是self-explanatory。有些情况下不 应用该规则会导致类型歧义。

如果您按照上面的建议禁用扩展程序,您 收到类型错误 试图编译上述声明。然而,这并不是真正的问题: 你已经知道在使用 read 时你必须以某种方式告诉编译器 它应该尝试解析哪种类型...

第二条规则

  1. Any monomorphic type variables that remain when type inference for an entire module is complete, are considered ambiguous, and are resolved to particular types using the defaulting rules (Section 4.3.4 ).

这个意思。如果你有你通常的定义:

plus = (+)

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

最终结果为:plus :: Integer -> Integer -> Integer.

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

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

plus = (+)

x = plus 1.0 2.0

在模块中,before 类型默认 plus 的类型将是: Fractional a => a -> a -> a(请参阅规则 1 了解发生这种情况的原因)。 此时,按照默认规则,a 将被替换为 Double 所以我们将有 plus :: Double -> Double -> Doublex :: Double.

默认

如前所述,存在一些 默认 规则,在 Section 4.3.4 of the Report 中有所描述, 推理器可以采用,并将用单态类型替换多态类型。 只要类型 不明确 .

就会发生这种情况

例如在表达式中:

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

这里的表达式不明确,因为 showread 的类型是:

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 类型 class 来处理数字, 有很多 数值表达式包含歧义的情况。

考虑:

show 1

结果应该是什么?

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

几乎每次我们使用数字时都会出现编译器错误并不是一件好事, 因此引入了违约规则。规则可控 使用 default 声明。通过指定 default (T1, T2, T3) 我们可以改变 推理器如何默认不同类型。

一个不明确的类型变量 v 是默认的,如果:

  • v 仅出现在 C v 类型的约束中 C 是 class (即,如果它显示为:Monad (m v) 那么它是 不可 可默认的。
  • 这些 class 中至少有一个是 NumNum 的子class。
  • 所有这些 class 都在 Prelude 或标准库中定义。

默认类型变量被default列表中的第一个类型替换 这是所有歧义变量的实例 classes.

默认的 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 不是数字 class.

扩展默认设置

注意 GHCi 自带 extended defaulting rules (or here for GHC8), 也可以使用 ExtendedDefaultRules 扩展在文件中启用。

默认类型变量不需要出现在所有的约束中 class 是标准的,必须至少有一个 class EqOrdShowNum 及其子classes.

而且默认的 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 约束导致 默认值 () 几乎没用。

有用的链接

关于单态限制有很多资源和讨论。

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