【问题标题】:NoMonomorphismRestriction helps preserve sharing?NoMonomorphismRestriction 有助于保持共享?
【发布时间】:2012-06-24 03:28:38
【问题描述】:

当我偶然发现这种奇怪的行为时,我正试图回答 another question 关于多态性与共享的问题。

在GHCi中,当我显式定义一个多态常量时,它并没有得到任何共享,这是可以理解的:

> let fib :: Num a => [a]; fib = 1 : 1 : zipWith (+) fib (tail fib)
> fib !! 30
1346269
(5.63 secs, 604992600 bytes)

另一方面,如果我尝试通过省略类型签名并禁用单态限制来达到同样的效果,我的常量会突然被共享!

> :set -XNoMonomorphismRestriction
> let fib = 1 : 1 : zipWith (+) fib (tail fib)
> :t fib
fib :: Num a => [a]
> fib !! 50
20365011074
(0.00 secs, 2110136 bytes)

为什么?!

呃...当使用优化编译时,即使禁用了单态限制,它也很快。

【问题讨论】:

  • 顺便说一句:关于 ghci 性能的推理有点奇怪——它 a) 比 ghc 本身慢 30 倍,b) 任何现实世界的代码都会使用优化,所以在 ghci 中学到的教训不会没那么有用。

标签: haskell ghc data-sharing monomorphism-restriction


【解决方案1】:

通过给出明确的类型签名,您可以防止 GHC 对您的代码做出某些假设。我将展示一个示例(取自question):

foo (x:y:_) = x == y
foo [_]     = foo []
foo []      = False

根据 GHCi,此函数的类型是 Eq a => [a] -> Bool,正如您所期望的那样。但是,如果您使用此签名声明 foo,您将收到“模糊类型变量”错误。

这个函数只能在没有类型签名的情况下工作的原因是因为类型检查在 GHC 中是如何工作的。当您省略类型签名时,foo 被假定为具有某些固定类型 a 的单型 [a] -> Bool。一旦你完成了绑定组的输入,你就可以概括这些类型。这就是您获得forall a. ... 的地方。

另一方面,当您声明多态类型签名时,您明确声明 foo 是多态的(因此 [] 的类型不必与第一个参数的类型匹配)并且繁荣,您获取不明确的类型变量。

现在,知道了这一点,让我们比较一下核心:

fib = 0:1:zipWith (+) fib (tail fib)
-----
fib :: forall a. Num a => [a]
[GblId, Arity=1]
fib =
  \ (@ a) ($dNum :: Num a) ->
    letrec {
      fib1 [Occ=LoopBreaker] :: [a]
      [LclId]
      fib1 =
        break<3>()
        : @ a
          (fromInteger @ a $dNum (__integer 0))
          (break<2>()
           : @ a
             (fromInteger @ a $dNum (__integer 1))
             (break<1>()
              zipWith
                @ a @ a @ a (+ @ a $dNum) fib1 (break<0>() tail @ a fib1))); } in
    fib1

第二个:

fib :: Num a => [a]
fib = 0:1:zipWith (+) fib (tail fib)
-----
Rec {
fib [Occ=LoopBreaker] :: forall a. Num a => [a]
[GblId, Arity=1]
fib =
  \ (@ a) ($dNum :: Num a) ->
    break<3>()
    : @ a
      (fromInteger @ a $dNum (__integer 0))
      (break<2>()
       : @ a
         (fromInteger @ a $dNum (__integer 1))
         (break<1>()
          zipWith
            @ a
            @ a
            @ a
            (+ @ a $dNum)
            (fib @ a $dNum)
            (break<0>() tail @ a (fib @ a $dNum))))
end Rec }

使用显式类型签名,与上面的foo 一样,GHC 必须将fib 视为潜在的多态递归值。我们可以将一些不同的Num 字典传递给zipWith (+) fib ... 中的fib,此时我们将不得不丢弃大部分列表,因为不同的Num 意味着不同的(+)。当然,一旦您使用优化进行编译,GHC 会注意到 Num 字典在“递归调用”期间永远不会更改并将其优化掉。

在上面的核心中,你可以看到GHC确实一次又一次地给fib一个Num字典(命名为$dNum)。

因为没有类型签名的fib 在整个绑定组的泛化完成之前被假定为单态,所以fib 子部分被赋予与整个fib 完全相同的类型。多亏了这一点,fib 看起来像:

{-# LANGUAGE ScopedTypeVariables #-}
fib :: forall a. Num a => [a]
fib = fib'
  where
    fib' :: [a]
    fib' = 0:1:zipWith (+) fib' (tail fib')

而且由于类型保持固定,您可以只使用一开始给出的字典。

【讨论】:

  • 啊哈!这解释了很多。谢谢!
【解决方案2】:

在这里,您在这两种情况下都使用具有相同类型参数的fib,而 ghc 足够聪明,可以看到并执行共享。

现在,如果您使用可以使用 不同 类型参数调用它的函数,并且默认导致其中一个与另一个非常不同,那么缺乏单态限制会咬你.

考虑在没有单态限制的两个上下文中多态地使用术语x = 2 + 2,其中在一个上下文中您使用show (div x 2),在另一个上下文中使用show (x / 2),在一个设置中您会得到IntegralShow 约束这导致您默认为Integer,在另一个中您获得FractionalShow 约束,并且默认您为Double,因此计算结果不会共享,因为您正在使用适用于两种不同类型的多态术语。开启单态限制后,它会尝试对 Integral 和 Fractional 都默认一次并失败。

请注意,这些天来让所有这些都被激发,而不是泛化等等。

【讨论】:

    猜你喜欢
    • 2015-12-17
    • 2014-10-13
    • 2016-04-27
    • 2017-11-09
    • 1970-01-01
    • 2011-04-20
    • 2011-07-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多