Haskell 哈斯克尔';什么是懒惰?

Haskell 哈斯克尔';什么是懒惰?,haskell,Haskell,考虑将列表中的所有元素加倍的函数: doubleMe [] = [] doubleMe (x:xs) = (2*x):(doubleMe xs) 然后考虑表达式 doubleMe (doubleMe [a,b,c]) 显然,在运行时,这首先扩展到: doubleMe ( (2*a):(doubleMe [b,c]) ) case doubleMe [a,b,c] of [] -> [] (x:xs) -> (2*x):(doubleMe xs) (这很明显,因为据我所

考虑将列表中的所有元素加倍的函数:

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

然后考虑表达式

doubleMe (doubleMe [a,b,c])
显然,在运行时,这首先扩展到:

doubleMe ( (2*a):(doubleMe [b,c]) )
case doubleMe [a,b,c] of
  [] -> []
  (x:xs) -> (2*x):(doubleMe xs)
(这很明显,因为据我所知,不存在其他可能性)

但我的问题是:为什么这一点现在扩展到了

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

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

凭直觉,我知道答案:因为哈斯克尔很懒。但是有人能给我一个更准确的答案吗

列表是否有什么特别的地方导致了这一点,或者这个想法比列表更普遍?

doubleMe(doubleMe[a,b,c])
没有扩展到
doubleMe((2*a):(doubleMe[b,c])
。它扩展到:

doubleMe ( (2*a):(doubleMe [b,c]) )
case doubleMe [a,b,c] of
  [] -> []
  (x:xs) -> (2*x):(doubleMe xs)
即首先展开外部函数调用。这就是懒惰语言和严格语言之间的主要区别:在扩展函数调用时,您不首先计算参数,而是用函数体替换函数调用,并暂时保持参数不变

现在需要扩展
doubleMe
,因为模式匹配需要知道其操作数的结构,然后才能对其求值,因此我们得到:

case (2*a):(doubleMe [b,c]) of
  [] -> []
  (x:xs) -> (2*x):(doubleMe xs)
现在模式匹配可以替换为第二个分支的主体,因为我们现在知道第二个分支是匹配的。因此,我们用
(2*a)
代替
x
,用
doubleMe[b,c]
代替
xs
,我们得到:

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

这就是我们得到结果的方式。

现在是进行等式推理的好时机,这意味着我们可以用一个函数来代替它的定义(模重命名事物以避免冲突)。为了简洁起见,我将把
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]
现在,如果我们用两层
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]
或者,您可以选择在不同的时间点减少,从而

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]
这是此计算的两种可能的扩展,但并不特定于列表。您可以将其应用于树类型:

data Tree a = Leaf a | Node a (Tree a) (Tree a)
<> >在<>代码>叶>代码>和<代码>节点< /代码>中,如果分别考虑

的列表定义,则分别对应于<代码> > [C]>和>:。
data [] a = [] | a : [a]

我之所以说这是两种可能的扩展,是因为它的扩展顺序取决于您正在使用的编译器的特定运行时和优化。如果它看到一个优化,使您的程序执行更快,它可以选择该优化。这就是为什么懒惰通常是一种恩惠,你不必考虑事情发生的顺序,因为编译器会帮你考虑。这在没有纯洁性的语言中是不可能的,例如C#/Java/Python/等。您不能重新安排计算,因为这些计算可能会产生依赖于顺序的副作用。但是,当执行纯计算时,不会产生副作用,因此编译器在优化代码时会更轻松。

之所以这样做,是因为列表的定义方式和惰性。当您请求列表的标题时,它会计算您请求的第一个元素,并保存其余元素以供以后使用。所有列表处理操作都基于head:rest概念,因此中间结果永远不会出现。

您的“明显的”第一步实际上并不那么明显。事实上,情况是这样的:

doubleMe (...)
doubleMe ( { [] | (_:_) }? )
doubleMe ( doubleMe (...)! )
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
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
...
只有在这一点上,它才真正“进入”内部函数。所以它继续前进

doubleMe ( doubleMe (...) )
doubleMe ( doubleMe( { [] | (_:_) }? ) )
doubleMe ( doubleMe( a:_ ! ) )
doubleMe ( (2*a) : doubleMe(_) )
doubleMe ( (2*a):_ ! )
现在,外部的
doubleMe
函数对它的
[]|(:)
问题有了“答案”,这是内部函数中的任何内容都被计算的唯一原因

实际上,下一步也不一定是你所想的:它取决于你如何评估外部结果!例如,如果整个表达式是
tail$doubleMe(doubleMe[a,b,c])
,那么它实际上会像

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

i、 事实上,它永远不会真正达到
2*a

Write\lambda y.m表示doubleMe的抽象版本,t表示列表[a,b,c]。那么你想减少的术语是

\y.m (\y.m t)
换句话说,有两个redex。Haskell更喜欢首先触发最外层的Redex,因为这是一种正常的order-ish语言。然而,事实并非如此。doubleMe不是真正的\y.m,只有当它的“参数”具有正确的形状(列表的形状)时才真正具有redex。由于这还不是一个redex,并且(\y.m)中没有redex,我们移到应用程序的右侧。因为Haskell也更倾向于首先评估最左边的重新定义。现在,t确实具有列表的形状,因此redex(\y.mt)将触发

然后我们回到顶端,再做一遍。除此之外,最外层的术语有redex

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

doubleMe (doubleMe [a,b,c])
我认为不同的人会以不同的方式扩展这些功能。我并不是说它们会产生不同的结果或任何东西,只是说在正确使用的人中,并没有真正的标准符号。我会这样做:

-- 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. 
这和sepp2k和leftaroundabout的答案一样,只是他们写的很有趣。sepp2k的答案有一个
case
表达式,似乎不知从何而来
doubleMe
的多等式定义被隐式重写为一个
case
表达式。leftaroundabout的答案中有一个
{[]}(:}?
的东西,它显然是一个符号,表示“我必须强制参数,直到它看起来像
[]
(:}

bhelkir的答案与我的类似,但它也递归地强制执行结果的所有子表达式,除非有消费者要求,否则不会发生这种情况


所以我对任何人都不尊重,但我更喜欢我的-其他人已经回答了这个一般性问题。让我补充一下这一点: