具有类约束类型的值实际上会在运行时成为函数吗?

Posted

技术标签:

【中文标题】具有类约束类型的值实际上会在运行时成为函数吗?【英文标题】:Will a value that has a type with class constraints actually be a function at run time? 【发布时间】:2011-12-01 08:53:37 【问题描述】:

考虑著名的

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 个数字。 (很遗憾,我目前无法尝试)

【问题讨论】:

我相信会有人来详细解释,但是你想在这里谷歌的术语是“thunk”。 @MatrixFrog - 不幸的是,不,这不会有太大帮助 - thunk 用于非严格语言的实现对我来说并不是什么新闻。关键是当我们限制@9​​87654333@ 时,列表中的thunk 可以实际更新,例如(fibs !! 42) 被评估。我的问题是,当fibs 是多态的时,是否以及如何做到这一点。 "由于重载的数字文字,它的类型为fibs :: Num a => [a]"。实际上由于单态限制和类型默认,它将具有类型[Integer] @sepp2k - 是的,没错。但是假设类型注释是明确给出的。我会相应地编辑问题。 尾部与 fibs 的返回相匹配,因此会发生共享,您会得到 O(n) 行为。 【参考方案1】:

这取决于实现。在 GHC 中,类型类是使用 dictionaries 实现的。假设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
<huge number>
(2.19 secs, 688490584 bytes)

但它仍然存在fibs 在单独的调用之间不共享的相同问题。如果你想要这个,你必须将fibs 专门化为letwhere 中所需的号码类型。

这些性能上的惊喜是可怕的monomorphism restriction 存在的部分原因。

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

【讨论】:

【参考方案2】:

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

您的陈述:

这似乎暗示在运行时,列表值 fibs 并不真正存在,而是一个函数,每次选取 fibs 的元素时都会重新计算列表?

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

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

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

【讨论】:

【参考方案3】:

函数总是涉及(-&gt;) 类型的构造函数,所以它不是函数。这是一个价值。函数也是值,但值不是函数,这与惰性无关。函数的关键属性是您可以应用它。应用程序的类型:

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

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

【讨论】:

换一种说法,你根本不知道它是如何实际实现的。不过,感谢您的参与。【参考方案4】:

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

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

【讨论】:

这似乎间接支持了我的观点,请参阅编辑后的问题。

以上是关于具有类约束类型的值实际上会在运行时成为函数吗?的主要内容,如果未能解决你的问题,请参考以下文章

C 语言Struct 实现运行类型识别 RTTI

Block运行时的类型以及Block是对象的验证

以编程方式为不同大小类创建具有不同常量的自动布局约束

自动布局黄色警告。它会在运行时使我的应用程序崩溃吗

具有本机 UML 类型的模板类和模板类的 OCL 约束

c语言中形参和实参的区别是啥?