考虑着名的
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个数字。
(不幸的是,我现在不能尝试这个)
答案 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
不在单独的调用之间共享。如果您需要此功能,则必须在fibs
或let
中将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使用函数的真实数学概念,这与在机器级别执行哪些策略事物完全无关。