简体   繁体   English

Haskell的懒惰如何运作?

[英]How does Haskell's laziness work?

Consider this function that doubles all the elements in a list: 考虑这个将列表中所有元素加倍的函数:

doubleMe [] = []
doubleMe (x:xs) = (2*x):(doubleMe xs)

Then consider the expression 然后考虑表达式

doubleMe (doubleMe [a,b,c])

It seems obvious that, at runtime, this first expands to: 很明显,在运行时,这首先扩展为:

doubleMe ( (2*a):(doubleMe [b,c]) )

(It's obvious because no other possibilities exist as far as I can see). (很明显,因为据我所知,没有其他可能性存在)。

But my question is this: Why exactly does this now expand to 但我的问题是: 为什么现在这个扩展到了

2*(2*a) : doubleMe( doubleMe [b,c] )

instead of 代替

doubleMe( (2*a):( (2*b) : doubleMe [c] ) )

?

Intuitively, I know the answer: Because Haskell is lazy. 直觉上,我知道答案:因为Haskell很懒惰。 But can someone give me a more precise answer? 但有人可以给我一个更准确的答案吗?

Is there something special about lists that causes this, or is the idea more general than that just lists? 是否有一些特殊的关于导致这种情况的列表,或者这个想法比仅仅列表更通用?

doubleMe (doubleMe [a,b,c]) does not expand to doubleMe ( (2*a):(doubleMe [b,c]) ) . doubleMe (doubleMe [a,b,c])不会扩展到doubleMe ( (2*a):(doubleMe [b,c]) ) It expands to: 它扩展到:

case doubleMe [a,b,c] of
  [] -> []
  (x:xs) -> (2*x):(doubleMe xs)

That is the outer function call is expanded first. 这是外部函数调用首先扩展。 That's the main difference between a lazy language and a strict one: When expanding a function call, you don't first evaluate the argument - instead you replace the function call with its body and leave the argument as-is for now. 这是惰性语言和严格语言之间的主要区别:扩展函数调用时,不首先评估参数 - 而是将函数调用替换为其主体,并将参数保留为现在。

Now the doubleMe needs to be expanded because the pattern matching needs to know the structure of its operand before it can be evaluated, so we get: 现在doubleMe需要扩展,因为模式匹配需要知道其操作数的结构才能进行评估,因此我们得到:

case (2*a):(doubleMe [b,c]) of
  [] -> []
  (x:xs) -> (2*x):(doubleMe xs)

Now the pattern matching can be replaced with the body of the second branch because we now know that the second branch is the one that matches. 现在模式匹配可以用第二个分支的主体替换,因为我们现在知道第二个分支是匹配的分支。 So we substitute (2*a) for x and doubleMe [b, c] for xs , giving us: 所以我们用x替换(2*a)xs doubleMe [b, c] ,给我们:

(2*(2*a)):(doubleMe (doubleMe [b,c]))

So that's how we arrive at that result. 这就是我们如何得出结果。

Your “obvious” first step isn't actually quite so obvious. 你的“显而易见的”第一步实际上并不那么明显。 In fact what happens is rather like this: 实际上发生的事情是这样的:

doubleMe (...)
doubleMe ( { [] | (_:_) }? )
doubleMe ( doubleMe (...)! )

and only at that point does it actually “enter” the inner function. 并且只有在那一点它才真正“进入”内部功能。 So it proceeds 所以它继续下去

doubleMe ( doubleMe (...) )
doubleMe ( doubleMe( { [] | (_:_) }? ) )
doubleMe ( doubleMe( a:_ ! ) )
doubleMe ( (2*a) : doubleMe(_) )
doubleMe ( (2*a):_ ! )

now here the outer doubleMe function has the “answer” to it's [] | (_:_) 现在这里的外部doubleMe函数对它的[] | (_:_)有“答案” [] | (_:_) question, which was the only reason anything in the inner function was evaluated at all. [] | (_:_)问题,这是内部函数的任何内容被评估的唯一原因。

Actually, the next step is also not necessarily what you though: it depends on how you evaluate the outer result! 实际上,下一步也不一定是你的意思:它取决于你如何评估外部结果! For instance, if the whole expression was tail $ doubleMe ( doubleMe [a,b,c] ) , then it would actually expand more like 例如,如果整个表达式是tail $ doubleMe ( doubleMe [a,b,c] ) ,那么它实际上会更像

tail( { [] | (_:_) }? )
tail( doubleMe(...)! )
tail( doubleMe ( { [] | (_:_) }? ) )
...
tail( doubleMe ( doubleMe( a:_ ! ) ) )
tail( doubleMe ( _:_ ) )
tail( _ : doubleMe ( _ ) )
doubleMe ( ... )

ie it would in fact never really get to 2*a ! 即它实际上永远不会真正达到2*a

Others have already answered the general question. 其他人已经回答了一般性问题。 Let me add something on this specific point: 让我在这一点上添加一些内容:

Is there something special about lists that causes this, or is the idea more general than that just lists? 是否有一些特殊的关于导致这种情况的列表,或者这个想法比仅仅列表更通用?

No, lists are not special. 不,名单并不特别。 Every data type in Haskell has a lazy semantics. Haskell中的每种data类型都具有惰性语义。 Let's try a simple example using the pair type for integers (Int, Int) . 让我们尝试使用整数对的类型(Int, Int)的简单示例。

let pair :: (Int,Int)
    pair = (1, fst pair)
 in snd pair

Above, fst,snd are the pair projections, returning the first/second component of a pair. 在上面, fst,snd是一对投影,返回一对的第一/第二组成部分。 Also note that pair is a recursively defined pair. 另请注意, pair是递归定义的对。 Yes, in Haskell you can recursively define everything, not just functions. 是的,在Haskell中,您可以递归地定义所有内容,而不仅仅是函数。

Under a lazy semantics, the above expression is roughly evaluated like this: 在惰性语义下,上面的表达式粗略地评估如下:

snd pair
= -- definition of pair
snd (1, fst pair)
= -- application of snd
fst pair
= -- definition of pair
fst (1, fst pair)
= -- application of fst
1

By comparison, using an eager semantics, we would evaluate it like this: 相比之下,使用急切的语义,我们会像这样评估它:

snd pair
= -- definition of pair
snd (1, fst pair)
= -- must evaluate arguments before application, expand pair again
snd (1, fst (1, fst pair))
= -- must evaluate arguments
snd (1, fst (1, fst (1, fst pair)))
= -- must evaluate arguments
...

In the eager evaluation, we insist on evaluating arguments before applying fst/snd , and we obtain a infinitely looping program. 在热切的评估中,我们坚持在应用fst/snd之前评估参数,并且我们获得了无限循环的程序。 In some languages this will trigger a "stack overflow" error. 在某些语言中,这将触发“堆栈溢出”错误。

In the lazy evaluation, we apply functions soon, even if the argument is not fully evaluated. 在惰性求值中,即使参数未得到充分评估,我们也会很快应用函数。 This makes snd (1, infiniteLoop) return 1 immediately. 这使得snd (1, infiniteLoop)立即返回1

So, lazy evaluation is not specific to lists. 因此,懒惰评估不是特定于列表。 Anything is lazy in Haskell: trees, functions, tuples, records, user-defined data types, etc. Haskell中任何东西都是懒惰的:树,函数,元组,记录,用户定义的data类型等。

(Nitpick: if the programmer really asks for them, it is possible to define types having strict / eagerly-evaluated components. This can be done using strictness annotations, or using extensions such as unboxed types. While sometimes these have their uses, they're are not commonly found in Haskell programs.) (Nitpick:如果程序员真的要求它们,就可以定义具有严格/热切评估组件的类型。这可以使用严格注释,或使用扩展类型等扩展来完成。虽然有时它们有用途,但它们'在Haskell程序中不常见。)

This is a good time to pull out equational reasoning, which means we can substitute a function for its definition (modulo renaming things to not have clashes). 这是推出等式推理的好时机,这意味着我们可以用一个函数替换它的定义(模数重命名的东西没有冲突)。 I'm going to rename doubleMe to d for brevity, though: doubleMe ,为了简洁,我将把doubleMe重命名为d

d [] = []                           -- Rule 1
d (x:xs) = (2*x) : d xs             -- Rule 2

d [1, 2, 3] = d (1:2:3:[])
            = (2*1) : d (2:3:[])    -- Rule 2
            = 2 : d (2:3:[])        -- Reduce
            = 2 : (2*2) : d (3:[])  -- Rule 2
            = 2 : 4 : d (3:[])      -- Reduce
            = 2 : 4 : (2*3) : d []  -- Rule 2
            = 2 : 4 : 6 : d []      -- Reduce
            = 2 : 4 : 6 : []        -- Rule 1
            = [2, 4, 6]

So now if we were to perform this with 2 layers of doubleMe / d : 所以现在如果我们用2层doubleMe / d执行此操作:

d (d [1, 2, 3]) = d (d (1:2:3:[]))
                = d ((2*1) : d (2:3:[]))    -- Rule 2 (inner)
                = d (2 : d (2:3:[]))        -- Reduce
                = (2*2) : d (d (2:3:[]))    -- Rule 2 (outer)
                = 4 : d (d (2:3:[]))        -- Reduce
                = 4 : d ((2*2) : d (3:[]))  -- Rule 2 (inner)
                = 4 : d (4 : d (3:[]))      -- Reduce
                = 4 : 8 : d (d (3:[]))      -- Rule 2 (outer) / Reduce
                = 4 : 8 : d (6 : d [])      -- Rule 2 (inner) / Reduce
                = 4 : 8 : 12 : d (d [])     -- Rule 2 (outer) / Reduce
                = 4 : 8 : 12 : d []         -- Rule 1 (inner)
                = 4 : 8 : 12 : []           -- Rule 1 (outer)
                = [4, 8, 12]

Alternatively, you can choose to reduce at different points in time, resulting in 或者,您可以选择在不同的时间点减少,从而产生

d (d [1, 2, 3]) = d (d (1:2:3:[]))
                = d ((2*1) : d (2:3:[]))
                = (2*(2*1)) : d (d (2:3:[]))
                = -- Rest of the steps left as an exercise for the reader
                = (2*(2*1)) : (2*(2*2)) : (2*(2*3)) : []
                = (2*2) : (2*4) : (2*6) : []
                = 4 : 6 : 12 : []
                = [4, 6, 12]

These are two possible expansions for this computation, but it's not specific to lists. 这是计算的两种可能的扩展,但它不是特定于列表。 You could apply it to a tree type: 您可以将它应用于树类型:

data Tree a = Leaf a | Node a (Tree a) (Tree a)

Where pattern matching on Leaf and Node would be akin to matching on [] and : respectively, if you consider the list definition of 如果考虑列表定义,则LeafNode上的模式匹配类似于[]:上的匹配

data [] a = [] | a : [a]

The reason why I say that these are two possible expansions is because the order in which it is expanded is up to the specific runtime and optimizations for the compiler you're using. 我之所以说这两个可能的扩展是因为它的扩展顺序取决于您正在使用的编译器的特定运行时和优化。 If it sees an optimization that would make your program execute much faster it can choose that optimization. 如果它看到优化会使您的程序执行得更快,那么它可以选择优化。 This is why laziness is often a boon, you don't have to think about the order in which things occurs as much because the compiler does that thinking for you. 这就是为什么懒惰往往是一个福音,你不必考虑事情发生的顺序,因为编译器会为你思考。 This wouldn't be possible in a language without purity, such as C#/Java/Python/etc. 对于没有纯度的语言,例如C#/ Java / Python /等,这是不可能的。 You can't rearrange computations since those computations might have side effects that depend on the order. 您无法重新排列计算,因为这些计算可能具有依赖于顺序的副作用。 But when performing pure calculations you don't have side effects and so the compiler has an easier job at optimizing your code. 但是在执行纯计算时,您没有副作用,因此编译器可以更轻松地优化代码。

doubleMe [] = []
doubleMe (x:xs) = (2*x):(doubleMe xs)

doubleMe (doubleMe [a,b,c])

I think different people expand these differently. 我认为不同的人会以不同的方式展开 I don't meant that they produce different results or anything, just that among people who do it correctly there isn't really a standard notation. 我并不是说它们产生不同的结果或任何东西,只是正确地做这些的人并没有真正的标准符号。 Here's how I would do it: 我是这样做的:

-- Let's manually compute the result of *forcing* the following expression.
-- ("Forcing" = demanding that the expression be evaluated only just enough
-- to pattern match on its data constructor.)
doubleMe (doubleMe [a,b,c])

    -- The argument to the outer `doubleMe` is not headed by a constructor,
    -- so we must force the inner application of `doubleMe`.  To do that, 
    -- first force its argument to make it explicitly headed by a
    -- constructor.
    = doubleMe (doubleMe (a:[b,c]))

    -- Now that the argument has been forced we can tell which of the two
    -- `doubleMe` equations applies to it: the second one.  So we use that
    -- to rewrite it.
    = doubleMe (2*a : doubleMe [b,c])

    -- Since the argument to the outer `doubleMe` in the previous expression
    -- is headed by the list constructor `:`, we're done with forcing it.
    -- Now we use the second `doubleMe` equation to rewrite the outer
    -- function application. 
    = 2*2*a : doubleMe (doubleMe [b, c])

    -- And now we've arrived at an expression whose outermost operator
    -- is a data constructor (`:`).  This means that we've successfully 
    -- forced the expression, and can stop here.  There wouldn't be any
    -- further evaluation unless some consumer tried to match either of 
    -- the two subexpressions of this result. 

This is the same as sepp2k's and leftaroundabout's answers, just that they write it funny. 这与sepp2k和leftaroundabout的答案相同,只是他们写得很有趣。 sepp2k's answer has a case expression appearing seemingly out of nowhere—the multi-equational definition of doubleMe got implicitly rewritten as a single case expression. sepp2k的答案有一个看似无处不在的case表达式 - doubleMe的多等式定义被隐式重写为单个case表达式。 leftaroundabout's answer has a { [] | (_:_) }? leftaroundabout的答案有{ [] | (_:_) }? { [] | (_:_) }? thing in it which apparently is a notation for "I have to force the argument until it looks like either [] or (_:_) ". 其中的东西显然是“我必须强制论证直到看起来像[](_:_) ”的符号。

bhelkir's answer is similar to mine, but it's recursively forcing all of the subexpressions of the result as well, which wouldn't happen unless you have a consumer that demands it. bhelkir的答案与我的相似,但它递归地强制结果的所有子表达式,除非你有一个需要它的消费者,否则这种情况不会发生。

So no disrespect to anybody, but I like mine better. 所以不要对任何人不尊重,但我更喜欢我。 :-P :-P

Write \\lambda ym to denote the abstracted version of doubleMe, and t for the list [a,b,c]. 写\\ lambda ym表示doubleMe的抽象版本,t表示列表[a,b,c]。 Then the term you want to reduce is 那么你要减少的术语是

\y.m (\y.m t)

In other words, there are two redex. 换句话说,有两个redex。 Haskell prefers to fire outermost redexes first since it is a normal order-ish language. Haskell首先尝试触发最外层的redex,因为它是一种正常的order-ish语言。 However, this isn't quite true. 但是,这不是真的。 doubleMe isn't really \\ym, and only really has a redex when it's "argument" has the correct shape (that of a list). doubleMe不是真正的\\ ym,只有当它的“参数”具有正确的形状(列表的形状)时才真正具有重新索引。 Since this isn't yet a redex, and there are no redexes inside of (\\ym) we move to the right of the application. 由于这还不是redex,并且(\\ ym)中没有重新索引,我们移动到应用程序的右侧。 Since Haskell also would prefer to evaluate leftmost redexes first. 因为Haskell也希望首先评估最左边的重新索引。 Now, t really does have the shape of a list, so the redex (\\ym t) fires. 现在,确实具有列表的形状,因此redex(\\ ym t)会触发。

\y.m (a : (\y.m t'))

And then we go back to the top, and do the whole thing again. 然后我们回到顶部,再做一遍。 Except this time, the outermost term has a redex. 除此之外,最外面的术语有一个redex。

It does so because of how lists are defined and laziness. 它是这样做的,因为列表是如何定义和懒惰的。 When you ask for the head of the list it evaluates that first element you asked for and saves the rest for later. 当您要求列表的头部时,它会评估您要求的第一个元素,并保存其余元素以供日后使用。 All list processing operations are built on the head:rest concept, so intermediate results never come up. 所有列表处理操作都建立在head:rest概念上,因此中间结果永远不会出现。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM