我在下面做了一些简单的假设来理解Haskell中的列表延迟评估,
head [1, 2] -- expr1
head [1 .. 2] -- expr2
head [1 ..] -- expr3
head . (1 :) $ [] -- eval1
head . (1 :) . (2 :) $ [] -- eval2
我认为expr3
会像eval1
那样被懒惰地评估,expr1
和expr2
怎么样?
一般来说,
答案 0 :(得分:6)
术语"懒惰评估"在很多方面使用。
在其余部分,我将使用"懒惰评估"对于实施策略和"非严格语义"对于语义。
我认为
expr3
会像eval1
一样被懒惰地评估,expr1
和expr2
怎么样?
非严格语义规定对所有五个术语的评估应终止并生成值1
,因此任何符合要求的实现都将以这种方式运行。延迟评估将在每个表达式的大约相同的空间和时间内执行此操作。我希望GHC会选择延迟评估,如果您强制执行这五个术语中的任何一个,尽管优化它可能会在编译时执行评估。如果您对此感兴趣,可以通过传递-ddump-simpl
标志来自行检查。
Haskell中的懒惰评估是编译和运行时的技术吗?
希望上面的讨论已经澄清了这个问题。非严格语义描述了编译时和运行时之间的特定连接(也就是说,编译器必须生成一个程序,其运行时行为产生语义指定的值)。延迟评估是用于生成符合语义的程序的特定实现策略。 GHC有时会在其计划中使用惰性评估,但有时会使用其他实施策略;但是,它符合非严格的语义。 (如果你找到一个没有的地方,这就是一个错误!)
哪里说效率好,但很难推断,按时,空间复杂或程序逻辑?
非严格语义通常不会说明在计算过程中使用了多少时间或空间,所以如果你想对此进行推理,你需要完全不同的技术。即使您决定将您的推理限制为使用延迟评估实现的程序,事情也可能很困难。考虑像[1..]
这样的表达式:它使用了多少空间?这个问题不能在真空中回答;懒惰评估的基本思想是让消费者对价值构建的价值进行控制。因此,如果没有使用表达式[1..]
查看所做的程序,我们就不会知道多少。它可能会抛弃价值,在这种情况下几乎不会使用任何空间;或者它可能沿着列表走,在这种情况下使用恒定的空间;或者它可能在不同的时间遍历列表两次,在这种情况下使用无界空间;或者它可以做其他空间要求的其他一百万件。
答案 1 :(得分:2)
列表没什么特别之处。它们只是递归数据类型:
data [a] = a : [a] | []
现在当您使用[1 .. 2]
时,不直接转换为列表(1:(2:[]))
!它将存储为表达式[1 .. 2]
。
现在head
定义为:
head :: [a] -> a
head (x:_) = x
如果你调用head [1 .. 2]
,进入main
(因此Haskell被迫以某种方式对其进行评估),它会看到[1 .. 2]
不是数据结构,而是一个未解析的表达式,它将稍微解决一下表达式:
[1 .. 2] to (1:[(succ 1) .. 2])
因此现在读到:
head (1:[(succ 1) .. 2])
(请注意尾部仍然是一个表达式),但由于head
只对 - 好 - “头”感兴趣,它将返回1
。请注意,如果head
例如是1+2
,则不会立即将此评估为3
。
此外,如果你只是简单地调用head [1 .. 2]
表达式不会被评估,那么只有你想要显示结果时,Haskell才会努力计算它。
根据编译器实现,编译器可以在编译时努力传播常量(文字)并对它们执行操作,但由于编译器应始终遵循执行标准,因此语义保持不变。 / p>
答案 2 :(得分:2)
要完成其他答案,您可以使用ghci中的:sprint
命令检查延迟评估的工作方式:
Prelude> let xs = [1..10] :: [Int]
Prelude> :sprint xs
xs = _
Prelude> head xs
1
Prelude> :sprint xs
xs = 1 : _
Prelude> take 3 xs
[1,2,3]
Prelude> :sprint xs
xs = 1 : 2 : 3 : _
Prelude> length xs
10
Prelude> :sprint xs
xs = [1,2,3,4,5,6,7,8,9,10]