考虑一下著名的
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 中。
然而,据我所知,对于多态的fib
来说这是不可能的。 FUZxxl 的评论
因为类型类通常会引入一个新参数,其中包含
具有该类型类函数的字典
似乎支持我的观点,即像 fibs
这样的值在运行时有效地显示为函数?
如果是这样,那么像 ((maximum .map (fibs!!)) [100000 .. 101000] :: Integer)
这样的应用程序比非多态变体需要明显更长的时间来评估<代码>((最大.map(fibsInteger!!)) [100000 .. 101000] :: 整数)因为前 100000 个数字每次都必须重新计算。
(不幸的是,我现在不能尝试这个)
Consider the famous
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
Suppose that, to avoid the monomorphism restriction, this is annotated with:
fibs :: Num a => [a]
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?
The question is how such cases are actually handled in different Haskell implementations you know of.
--- ADDED ----
I feel that I must elaborate a bit more. Consider:
fibsInteger :: [Integer]
fibsInteger = 0: 1: zipWith (+) fibsInteger (tail fibsInteger)
and suppose during program execution the value
(fibsInteger !! 42)
needs to be evaluated. In that case I would expect that subsequent evaluations like that would find that the first 43 elements of fibsInteger
are already evaluated. This implies also that fibsInteger
itself and the first 42 tails of it are already in WHNF.
Yet that would not be possible with a polymorphic fibs
, as far as I can see. FUZxxl's remark
because a typeclass usually introduces a new argument containing a
dictionary with the functions of that typeclass
seems to support my view that a value like fibs
effectively appears as function at runtime?
If this were so, then an application like ((maximum . map (fibs!!)) [100000 .. 101000] :: Integer)
shold take noticeably longer to evaluate than the non-polymorphic variant ((maximum . map (fibsInteger!!)) [100000 .. 101000] :: Integer)
because the first 100000 numbers will have to be recomputed each time.
(Unfortunately, I can't try this out at this time)
发布评论
评论(4)
这取决于实施。在 GHC 中,类型类是使用字典来实现的。假设
Num
类的定义如下(本示例进行了简化):然后它将被编译为“字典”数据类型:
任何具有
Num
约束的内容都会得到字典的一个额外参数,例如foo x = x + 1
将变为:那么让我们看看 GHC 如何编译
fibs
,好吗?如果你稍微眯一下眼睛,这本质上就是这样,
所以对于 GHC 来说,答案是肯定的。正如您所怀疑的,这可能会对性能产生巨大影响,因为这会破坏此定义所依赖的
fib
共享,以至于您获得指数运行时间而不是线性1。我们可以通过引入自己的共享来解决这个问题:
这样要好得多。
但它仍然存在同样的问题,即
fibs
不在单独的调用之间共享。如果您想要这样,您必须在let
或where
中将fibs
专门化为您想要的数字类型。这些令人惊讶的性能是可怕的单态限制存在的部分原因。
1 忽略
Integer
加法不是恒定时间这一事实。It depends on the implementation. In GHC, type classes are implemented using dictionaries. Let's say the
Num
class was defined like this (simplified for this example):Then it will be compiled as a "dictionary" data type:
Anything with a
Num
constraint will get an extra argument for the dictionary, so for examplefoo x = x + 1
will become:So let's see how GHC compiles
fibs
, shall we?If you squint a little, this is essentially
So for GHC, the answer is yes. As you suspected, this can have drastic implications on performance, as this destroys the sharing of
fibs
that this definition relies on, to the point where you get exponential runtime instead of linear1.We can fix this problem by introducing sharing ourselves:
This is much better.
But it still has the same problem that
fibs
is not shared between separate calls. If you want this, you will have to specializefibs
to your desired number type in alet
orwhere
.These sort of performance surprises is part of the reason why the dreaded monomorphism restriction exists.
1 Ignoring the fact that
Integer
addition is not constant time.多态性会带来额外的性能负担(我认为这就是您要问的问题)。在托马斯对这个问题的回答< /a>,使类型成为非多态,将运行时间从 36 秒减少到 11 秒。
您的声明:
我不太确定你在这里的意思 - 你似乎意识到它很懒。您可能会问 Haskell 是否认为这是一个“函数声明”或“值声明” - 您可以尝试使用 Template Haskell:
所以它是一个值声明(ValD)。
Polymorphism can bring an additional performance burden (which I think is the question you are asking). In Thomas' answer to this question, making the type non-polymorphic reduced runtime from 36 to 11 seconds.
Your statement:
I'm not really sure what you mean here - you seem to be aware that it's lazy. You might be asking if Haskell considers this a "function declaration" or a "value declaration" - you can try using Template Haskell:
So it is a value declaration (ValD).
首先,列表是无限的,因此在程序运行之前不可能生成整个列表。正如 MatrixFrog 已经指出的那样,
fibs
是一个thunk。您可以粗略地将 thunk 想象为一个不带参数并返回值的函数。唯一的区别是,指向函数的指针随后被指向结果的指针替换,导致结果被缓存。仅当函数不依赖于任何类型类时才会发生这种情况,因为类型类通常会引入一个新参数,其中包含具有该类型类函数的字典(此过程有时称为具体化)。很长一段时间我发布了对此的答案 codegolf.SE 问题 包含 C 中 thunk 的自己实现。代码不是很好,列表内容与 thunk 本身没有很好地分离,但值得一看。
First of all, the list is infinite, thus it's impossible to generate the whole list before the program runs. As MatrixFrog already pointed out,
fibs
is a thunk. You can roughly imagine a thunk as a function that takes no argument and returns a value. The only difference is, that the pointer to the function is replaced by a pointer to the result afterwards, causing the result to be cached. This happens only in case of funcions that are not depending on any typeclass, because a typeclass usually introduces a new argument containing a dictionary with the functions of that typeclass (This process is sometimes called reification).A long time I posted an answer to this codegolf.SE question containing an own implementation of thunks in C. The code is not very good, the list stuff is not separated very well from the thunk itself, but it's worth having a look.
函数总是涉及
(->)
类型构造函数,因此它不是函数。这是一个价值。函数也是值,但值不是函数,这与惰性无关。函数的关键属性是您可以应用它。应用程序具有类型:当然,它是一个惰性值,并且在实现级别上涉及称为 thunk 的东西,但这在很大程度上与您的问题无关。 thunk 是一个实现细节。仅仅因为它是一个延迟计算的值并不能将其变成一个函数。不要将评估与执行混淆! C 等语言中的函数与 Haskell 中的函数不同。 Haskell 使用函数的真正数学概念,这与在机器级别上执行的策略完全无关。
A function always involves the
(->)
type constructor, so it is not a function. It's a value. Functions are also values, but values are not functions, and that has nothing to do with laziness. The key property of a function is that you can apply it. Application has the type:Of course it is a lazy value and on the implementation level involves something called a thunk, but this is largely irrelevant to your question. Thunks are an implementation detail. Just because it is a lazily computed value doesn't turn it into a function. Don't confuse evaluation with execution! Functions in a language like C are not the same as function in Haskell. Haskell uses the real mathematical notion of a function, which is completely unrelated to with which strategy things are executed on the machine level.