关于haskell:一个具有类约束的类型的值在运行时会是一个函数吗?

Will a value that has a type with class constraints actually be a function at run time?

想想著名的

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

假设为了避免单态限制,注释如下:

1
fibs :: Num a => [a]

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

问题是,您所知道的不同的haskell实现中如何处理这种情况。

------我觉得我必须再详细说明一点。考虑:

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

并假设在程序执行期间

1
(fibsInteger !! 42)

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

然而,据我所见,多态的fibs是不可能的。福茨克鲁的评论

because a typeclass usually introduces a new argument containing a
dictionary with the functions of that typeclass

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

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


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

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

然后将其编译为"字典"数据类型:

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

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

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

那么让我们看看GHC是如何编译fibs的,好吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ 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 }

如果你斜视一点,这基本上是

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

所以对于GHC来说,答案是肯定的。正如您所怀疑的,这可能会对性能产生重大影响,因为这会破坏此定义所依赖的fibs的共享,直到您得到指数运行时而不是线性1为止。

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

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

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

这样好多了。

1
2
3
4
5
6
7
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专门化到你想要的letwhere中的数字类型。

这些令人惊讶的性能是可怕的单态限制存在的部分原因。

1忽略Integeraddition is not constant time.


多态性会带来额外的性能负担(我认为这是您要问的问题)。在托马斯对这个问题的回答中,使类型非多态性将运行时间从36秒缩短到11秒。

你的声明:

This seems to imply that at runtime, a list value fibs does not really exist, but rather a function that computes the list anew each time an element of fibs is picked?

我真的不知道你在这里是什么意思-你似乎意识到这是懒惰的。您可能会问haskell是将其视为"函数声明"还是"值声明"——您可以尝试使用模板haskell:

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

所以它是一个值声明(vald)。


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

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

当然,它是一个懒惰的值,在实现级别上涉及到一个称为thunk的东西,但这在很大程度上与您的问题无关。thunk是一个实现细节。只是因为它是一个延迟计算的值,所以不会将其转换为函数。不要混淆评估和执行!C语言中的函数与Haskell中的函数不同。haskell使用了一个函数的真正数学概念,这与在机器级别执行策略完全无关。


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

很长一段时间,我发布了一个关于codegolf.se问题的答案,其中包含了自己在C语言中实现thunk的方法。代码不是很好,列表中的内容与thunk本身没有很好的分离,但值得一看。