具有类约束的类型的值实际上是否在运行时是一个函数?

时间:2011-10-05 10:22:33

标签: haskell

考虑着名的

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

假设为了避免单态性限制,这用注释:

fibs :: Num a => [a]

这似乎意味着在运行时,列表值fibs并不存在,而是每次选择fibs元素时重新计算列表的函数?

问题是如何在您知道的不同Haskell实现中实际处理此类案例。

---添加---- 我觉得我必须详细说明一下。考虑:

fibsInteger :: [Integer]
fibsInteger = 0: 1: zipWith (+) fibsInteger (tail fibsInteger)

并假设在程序执行期间值

(fibsInteger !! 42)

需要进行评估。在那种情况下,我希望像这样的后续评估会发现fibsInteger的前43个元素已经被评估过了。这也意味着fibsInteger本身及其前42个尾部已经在WHNF中。

然而,就我所知,多态fibs是不可能的。 FUZxxl的评论

  

因为类型类通常会引入一个包含a的新参数   具有该类型类功能的字典

似乎支持我的观点,fibs这样的值在运行时有效地显示为函数?

如果是这样,那么像((maximum . map (fibs!!)) [100000 .. 101000] :: Integer)这样的应用程序要比非多态变体((maximum . map (fibsInteger!!)) [100000 .. 101000] :: Integer)明显更长时间进行评估,因为每次都必须重新计算前100000个数字。 (不幸的是,我现在不能尝试这个)

4 个答案:

答案 0 :(得分:14)

这取决于实施。在GHC中,类型类是使用词典实现的。假设Num类是这样定义的(本例简化):

class Num a where
    fromInteger :: Integer -> a
    (+) :: a -> a -> a

然后它将被编译为“字典”数据类型:

data Num a = Num { fromInteger :: Integer -> a, plus :: a -> a -> a }

任何带有Num约束的内容都会为字典获得额外的参数,例如foo x = x + 1将成为:

foo :: Num a -> a -> a
foo num x = plus num x (fromInteger num 1)

那么让我们看看GHC如何编译fibs,我们呢?

$ cat Fibs.hs
module Fibs where
fibs :: Num a => [a]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
$ ghc -c Fibs.hs -ddump-simpl

==================== Tidy Core ====================
Rec {
Fibs.fibs [Occ=LoopBreaker]
  :: forall a_abu. GHC.Num.Num a_abu => [a_abu]
[GblId, Arity=1]
Fibs.fibs =
  \ (@ a_akv) ($dNum_akw :: GHC.Num.Num a_akv) ->
    GHC.Types.:
      @ a_akv
      (GHC.Num.fromInteger
         @ a_akv $dNum_akw (GHC.Integer.smallInteger 0))
      (GHC.Types.:
         @ a_akv
         (GHC.Num.fromInteger
            @ a_akv $dNum_akw (GHC.Integer.smallInteger 1))
         (GHC.List.zipWith
            @ a_akv
            @ a_akv
            @ a_akv
            (GHC.Num.+ @ a_akv $dNum_akw)
            (Fibs.fibs @ a_akv $dNum_akw)
            (GHC.List.tail @ a_akv (Fibs.fibs @ a_akv $dNum_akw))))
end Rec }

如果你眯一点,这基本上是

fibs :: Num a -> [a]
fibs num = fromInteger num 0
         : fromInteger num 1
         : zipWith (plus num) (fibs num) (tail (fibs num))

因此对于GHC来说,答案是肯定的。正如您所怀疑的那样,这会对性能产生严重影响,因为这会破坏此定义所依赖的fibs的共享,从而达到指数运行时而非线性 1 。< / p>

Prelude Fibs> :set +s
Prelude Fibs> fibs !! 30
832040
(3.78 secs, 912789096 bytes)

我们可以通过自己介绍来解决这个问题:

module SharedFibs where
fibs :: Num a => [a]
fibs = let f = 0 : 1 : zipWith (+) f (tail f) in f

这要好得多。

Prelude SharedFibs> :set +s
Prelude SharedFibs> fibs !! 30
832040
(0.06 secs, 18432472 bytes)
Prelude SharedFibs> fibs !! 100000
<huge number>
(2.19 secs, 688490584 bytes)

但它仍然存在相同的问题,即fibs不在单独的调用之间共享。如果您需要此功能,则必须在fibslet中将where专门化为所需的数字类型。

这些表现惊喜是可怕的monomorphism restriction存在的部分原因。

1 忽略Integer加法不是常数时间的事实。

答案 1 :(得分:1)

多态性会带来额外的性能负担(我认为这是你要问的问题)。在Thomas对this question的回答中,将非多态的类型从36秒减少到11秒。

您的陈述:

  

这似乎意味着在运行时,列表值fibs实际上并不存在,而是每次选择一个fibs元素时重新计算列表的函数?

我不确定你在这里的意思 - 你似乎意识到它是懒惰的。您可能会问Haskell是否认为这是“函数声明”或“值声明” - 您可以尝试使用Template Haskell:

> runQ [d| fib = 0 : 1 : zipWith (+) fib (tail fib) |]
[ValD (VarP fib) ...

所以这是一个价值声明(ValD)。

答案 2 :(得分:0)

首先,列表是无限的,因此在程序运行之前无法生成整个列表。正如MatrixFrog已经指出的那样,fibs thunk 。您可以粗略地将thunk想象为一个不带参数的函数并返回一个值。唯一的区别是,指向函数的指针后来被指向结果的指针替换,导致结果被缓存。这种情况只发生在不依赖于任何类型类的函数的情况下,因为类型类通常会引入一个包含具有该类型类函数的字典的新参数(此过程有时称为 reification )。

很长一段时间我发布了这个codegolf.SE question的回答,其中包含own implementation个C中的thunk。代码不是很好,列表内容与thunk本身并没有很好的分离,但是值得一看。

答案 3 :(得分:0)

函数总是涉及(->)类型构造函数,因此它不是函数。这是一个价值。函数也是值,但值不是函数,与懒惰无关。函数的关键属性是您可以应用它。应用程序具有以下类型:

(a -> b) -> a -> b

当然它是一个懒惰的值,并且在实现级别涉及一个叫做thunk的东西,但这在很大程度上与你的问题无关。 Thunks是一个实现细节。仅仅因为它是一个懒惰的计算值不会把它变成一个函数。不要将评估与执行混淆!像C这样的语言中的函数与Haskell中的函数不同。 Haskell使用函数的真实数学概念,这与在机器级别执行哪些策略事物完全无关。