首页 文章

什么是单态限制?

提问于
浏览
54

令我感到困惑的是,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中单形态限制的规范问题,如元问题中所讨论的 .

1 回答

  • 76

    什么是单态限制?

    Haskell wiki所述的monomorphism restriction是:

    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
    

    Note: 您应该更喜欢第一个选项,而不是通过命令行选项选择扩展 .

    有关此扩展名和其他扩展名的说明,请参阅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
    

    您可能需要:在较新的GHCi版本中设置-XMonomorphismRestriction .

    事实上,我们可以看到 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定义,有两种不同类型的bindings . 功能绑定和模式绑定 . 函数绑定只不过是函数的定义:

    f x = x + 1
    

    请注意,它们的语法是:

    <identifier> arg1 arg2 ... argn = expr
    

    Modulo守卫和声明 . 但它们并不重要 .

    哪里必须至少有一个参数 .

    模式绑定是表单的声明:

    <pattern> = expr
    

    同样,模数守卫 .

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

    plus = (+)
    

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

    当模式绑定仅由变量名称组成时,它称为简单模式绑定 .

    The monomorphism restriction applies to simple pattern bindings!

    好吧,我们应该正式说:

    声明组是一组最小的相互依赖绑定 .

    报告第4.5.1节 .

    然后(report的第4.5.5节):

    当且仅当以下情况时,给定的声明组不受限制:组中的每个变量都受到函数绑定(例如fx = x)或简单模式绑定(例如加号=();第4.4.3.2节)和明确的约束对于由简单模式绑定绑定的组中的每个变量,都给出了类型签名 . (例如加上:: Num a => a - > a - > a;加=()) .

    我添加的例子 .

    因此,受限制的声明组是一个组,其中有非简单的模式绑定(例如 (x:xs) = f something(f, g) = ((+), (-)) ),或者存在一些没有类型签名的简单模式绑定(如 plus = (+) ) .

    单态限制会影响 restricted 声明组 .

    大多数情况下,您不定义相互递归函数,因此声明组只是一个绑定 .

    它做什么?

    单态限制由report的4.5.5节中的两个规则描述 .

    第一条规则

    通常的Hindley-Milner对多态性的限制是,只有在环境中不会自由发生的类型变量才会被推广 . 此外,受限制的声明组的约束类型变量可能不会在该组的泛化步骤中进行推广 . (回想一下,如果类型变量必须属于某个类型类,则会受到约束;请参阅第4.5.2节 . )

    突出显示的部分是单态限制引入的内容 . 它说如果类型是多态的(即它包含一些类型变量)并且该类型变量受约束(即它对它有一个类约束:例如类型 Num a => a -> a -> a 是多态的,因为它包含 a 并且也是因为 a 具有约束 Num 在它上面 . )然后它不能一概而论 .

    简单来说,不泛化意味着 plus 函数的 uses 可能会改变其类型 .

    如果你有定义:

    plus = (+)
    
    x :: Integer
    x = plus 1 2
    
    y :: Double
    y = plus 1.0 2
    

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

    理由

    报告说:

    规则1是出于两个原因所必需的,这两个原因都非常微妙 . 规则1防止计算意外重复 . 例如,genericLength是一个标准函数(在库Data.List中),其类型由genericLength :: Num a => [b] - > a给出
    现在考虑以下表达式:let len = genericLength xs
    在(len,len)
    看起来len应该只计算一次,但是如果没有规则1,它可能会被计算两次,一次是在两次不同的重载中 . 如果程序员确实希望重复计算,可以添加显式类型签名:let len :: Num a => a
    len = genericLength xs
    在(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回想一下,读取是一个标准函数,其类型由签名读取::(读取)=>字符串 - > [(a,String)]如果没有规则1,将为n分配类型∀a . 读一个⇒a和s类型∀a . 读一个⇒字符串 . 后者是一种无效的类型,因为它本质上是模棱两可的 . 不可能确定使用s的重载,也不能通过为s添加类型签名来解决 . 因此,当使用非简单模式绑定时(第4.4.3.2节),无论是否提供类型签名,推断的类型在其约束类型变量中始终是单形的 . 在这种情况下,n和s在a中都是单态的 .

    好吧,我相信这个例子是不言自明的 . 在某些情况下,不应用规则结果类型歧义 .

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

    第二条规则

    当整个模块的类型推断完成时保留的任何单形类型变量都被认为是不明确的,并使用默认规则解析为特定类型(第4.3.4节) .

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

    plus = (+)
    

    这将具有 Num a => a -> a -> a 类型,其中 a 是由于上述规则1的单态类型变量 . 一旦推断出整个模块,编译器将根据默认规则选择一个将替换 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 .

    违约

    如前所述,在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 类型类来处理数字,因此很多情况下数值表达式包含歧义 .

    考虑:

    show 1
    

    结果应该是什么?

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

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

    如果出现以下情况,则不明确的类型变量 v 是默认的:

    • v 只出现在 C v 类型的约束中_1145297_是一个类(即如果它出现在: Monad (m v) 那么它是 not defaultable) .

    • 这些类中至少有一个是 NumNum 的子类 .

    • 所有这些类都在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附带extended defaulting rules(或herefor GHC8),可以使用 ExtendedDefaultRules 扩展名在文件中启用 .

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

    此外,默认的 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 约束导致 () 的默认值,这几乎没用 .

    有用的链接

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

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

相关问题