具有类约束的类型值在运行时实际上会是一个函数吗?
具有类约束的类型值在运行时实际上会是一个函数吗?
考虑这个著名的
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的评论
因为类型类通常引入一个包含该类型类函数的字典的新参数
似乎支持我的观点,即像fibs
这样的值在运行时实际上表现为函数?
如果是这样,那么像((maximum . map (fibs!!)) [100000 .. 101000] :: Integer)
这样的应用程序执行起来应该比非多态变量((maximum . map (fibsInteger!!)) [100000 .. 101000] :: Integer)
要慢得多,因为前100000个数字将必须每次重新计算。
(不幸的是,我此时无法尝试此操作)
多态性可能会带来额外的性能负担(我认为这就是你要问的问题)。在Thomas回答这个问题时,将类型设为非多态性可以将运行时间从36秒减少到11秒。
你的陈述:
这似乎意味着,在运行时,列表值fibs实际上并不存在,而是每次选择fibs元素时都会计算列表的新函数?
我不太确定你的意思-你似乎已经意识到它是惰性的。你可能想知道Haskell是否认为这是“函数声明”还是“值声明”-你可以尝试使用模板Haskell:
> runQ [d| fib = 0 : 1 : zipWith (+) fib (tail fib) |] [ValD (VarP fib) ...
所以它是一个值声明(ValD)。
这取决于具体的实现。在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。
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(2.19 secs, 688490584 bytes)
但它仍然有相同的问题,即fibs
在不同的调用间没有共享。如果你想要这个功能,你必须在let
或where
中将fibs
专门化为你想要的数字类型。
这种性能上的惊喜是为什么可怕的单态限制存在的一部分。
1 忽略Integer
加法不是常数时间的事实。