我们中的一些人正在阅读有关Haskell的一些内容,我们昨天正在讨论一些概念。问题是Haskell是一种懒惰的语言,它如何处理检索列表的第n个元素?
例如,如果我们有
[2,4..10000000] !! 200
它实际上是否会将列表填充到200个元素中?或者它将其编译成类似于
的等式n*step + firstValue
然后返回那个计算?之所以出现这种情况,是因为有人试图想出一个程序容易耗尽内存的例子,并且想要遍历一个(足够大)的列表是第一个出现的候选者。
答案 0 :(得分:12)
是的,它会在返回之前产生列表的前201个元素。但是,由于此列表无法从程序中的任何其他位置访问,因此初始位将有资格进行垃圾收集,因此它将以恒定的空间(但线性时间)运行,并且具有简单的实现。
当然,优化编译器可能会做得更好。由于表达式是常量,因此甚至可以在编译时对其进行求值。
答案 1 :(得分:9)
它实际上是否会将列表最多填充200个元素?
在一个天真的实现中,是的。
或者它是否将其编译成类似于
n*step + firstValue
的等式?
优化的Haskell编译器可能会这样做,但我不希望实际的实现执行此优化。
关键是Haskell是如此严格地形式化,以至于可以证明这两个选项在理想化机器上的返回值方面是等价的,因此编译器可以选择其中一个。语言标准( Haskell报告)只描述了应返回的值,而不是如何计算它。
答案 2 :(得分:7)
术语“懒惰”具有精确的数学意义,您可以通过需要lambda演算的书籍来学习。外行人的定义“在其他地方需要结果之前,没有任何评估”只是新手的隐喻。这是一种简化,所以在这种复杂的情况下,需要理解完整的理论来解释正在发生的事情。
精确语义要求编译器在对其执行模式匹配之前不评估列表元素。这不是优化问题 - 总是必须如此。所以,如果你计算一个!! 3,你得到的最小值(取决于a的定义)如下:
_:_:_:5:_
这里_是指“未评估”。通过学习lambda演算,您可以学习理解评估的内容和不评估的内容。在此之前,您可以使用GHCi调试器来查看:
Prelude> let l = [1..10]
Prelude> let x = l !! 5
Prelude> :set -fprint-evld-with-show
Prelude> :print x
x = (_t1::Integer)
Prelude> :print l
l = (_t2::[Integer])
Prelude> x
6
Prelude> :print l
l = 1 : 2 : 3 : 4 : 5 : 6 : (_t3::[Integer])
请注意,在打印x之前,根本不会对l进行评估。打印调用show,show执行一系列模式匹配。在这种特殊情况下,由于[1..10]实现中的模式匹配,列表的第一个元素得到了评估(实际上它被转换为通常的应用程序enumFromTo 1 10
)。但是,如果我们添加m = map(+1)l,我们注意到m的更多元素未被评估,因为map比[1..10]具有更少的模式匹配:
Prelude> let m = map (+1) l
Prelude> :print m
m = (_t4::[Integer])
Prelude> m !! 5
7
Prelude> :print m
m = (_t5::Integer) : (_t6::Integer) : (_t7::Integer) :
(_t8::Integer) : (_t9::Integer) : 7 : (_t10::[Integer])
我再说一遍,可以很容易地识别出被评估的内容和不被评估的内容,以及执行的确切顺序评估,但是你需要学习精确的语义 - 只是学习一个比喻并不能让你理解细节。最后一个例子是
> Prelude> let ll = zipWith (+) l (tail l) Prelude> ll !! 5 13 Prelude>
> :print l l = [1,2,3,4,5,6,7,8,9,10]
因此,取决于(静态已知!)程序的结构,很多情况都是可能的。至少在评估清单时! 3,你得到_ : _ : _ : 5 : _
。在最大程度上,您将获得评估的完整列表:1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : []
。
我可以很容易地构建所有这4个样本情况 - 所以你也可以学习,但它需要一些数学背景。
答案 3 :(得分:6)
正如larsmans所说,由编译器来决定做什么。但我希望GHC能够填充这个列表直到第201个元素。但它不会评估这些元素。
假设有一个阶乘函数:
factorial n = product [1..n]
以下代码将打印200的阶乘,它将创建列表的前201个单元格,但它只会评估一个阶乘。
print $ [ factorial n | n <- [0,1..] ] !! 201
答案 4 :(得分:6)
取决于-Ox中的x
import Criterion.Main
import qualified Data.Vector as V
import qualified Data.List.Stream as S
naive _ = [2,4 .. k] !! n
eq _ = n*2 + 2
uvector _ = V.enumFromThenTo 2 4 k V.! n
stream _ = [2,4 .. k] S.!! n
n = 100000
k = 10*n
main = defaultMain [ bgroup "range"
[ bench "naive" $ whnf naive n
, bench "eq" $ whnf eq n
, bench "uvector" $ whnf uvector n
, bench "stream" $ whnf stream n
]]
-- -Odph -fforce-recomp -fllvm
--
--benchmarking range/naive
--mean: 11.83244 ns, lb 11.39379 ns, ub 12.90468 ns, ci 0.950
--std dev: 3.304705 ns, lb 1.189680 ns, ub 6.155017 ns, ci 0.950
--
--benchmarking range/eq
--mean: 7.911626 ns, lb 7.741035 ns, ub 8.122809 ns, ci 0.950
--std dev: 970.2263 ps, lb 828.3840 ps, ub 1.177933 ns, ci 0.950
--
--benchmarking range/uvector
--mean: 10.74393 ns, lb 10.30107 ns, ub 11.81737 ns, ci 0.950
--std dev: 3.268982 ns, lb 861.2390 ps, ub 5.811662 ns, ci 0.950
--
--benchmarking range/stream
--mean: 12.34206 ns, lb 11.71146 ns, ub 14.07016 ns, ci 0.950
--std dev: 4.959039 ns, lb 2.124692 ns, ub 10.40687 ns, ci 0.950
-- -O3 -fforce-recomp -fasm
--benchmarking range/naive
--mean: 11.11646 ns, lb 10.83341 ns, ub 11.82991 ns, ci 0.950
--std dev: 2.048823 ns, lb 289.9484 ps, ub 3.752569 ns, ci 0.950
--
--benchmarking range/eq
--mean: 8.535535 ns, lb 8.297940 ns, ub 9.067161 ns, ci 0.950
--std dev: 1.771753 ns, lb 933.7552 ps, ub 2.843637 ns, ci 0.950
--
--benchmarking range/uvector
--mean: 11.12599 ns, lb 10.88839 ns, ub 11.71998 ns, ci 0.950
--std dev: 1.734431 ns, lb 306.4149 ps, ub 3.123837 ns, ci 0.950
--
--benchmarking range/stream
--mean: 10.73798 ns, lb 10.42936 ns, ub 11.45102 ns, ci 0.950
--std dev: 2.301690 ns, lb 1.184686 ns, ub 3.877275 ns, ci 0.950
-- -O0 -fforce-recomp -fasm
--benchmarking range/naive
--mean: 1.742292 ms, lb 1.693402 ms, ub 1.934525 ms, ci 0.950
--std dev: 432.1991 us, lb 70.44581 us, ub 1.006263 ms, ci 0.950
--
--benchmarking range/eq
--mean: 37.66248 ns, lb 36.37912 ns, ub 42.66504 ns, ci 0.950
--std dev: 11.91135 ns, lb 1.493463 ns, ub 28.17839 ns, ci 0.950
--
--benchmarking range/uvector
--mean: 36.32181 ms, lb 35.41175 ms, ub 38.63195 ms, ci 0.950
--std dev: 6.887482 ms, lb 2.532232 ms, ub 13.47616 ms, ci 0.950
--
--benchmarking range/stream
--mean: 1.731072 ms, lb 1.692072 ms, ub 1.875080 ms, ci 0.950
--std dev: 342.2325 us, lb 81.77006 us, ub 792.2414 us, ci 0.950
嗯,在这个简单的例子中,GHC(7.0.2)确实非常聪明。
答案 5 :(得分:3)
使用GHC,您可以编写像
这样的内容myVal = [2,4..] !! 200
查找无限列表中的元素。实际上,它没有分配完整列表。有关Haskell中内存泄漏的示例,请参阅http://www.haskell.org/haskellwiki/Memory_leak。
答案 6 :(得分:2)
我所知道的所有实现都会根据需要对列表进行打包。实际上,遍历足够大的列表很容易让你耗尽内存,你只需要安排它,这样你已经完成的部分就不会被垃圾收集,例如。
main :: IO ()
main = do
let xs :: [Int]
xs = [1 .. 10^9]
print (xs !! 123456789)
print (xs !! 2)
将其编译为fromIntegral n*step + start
是一项棘手的工作。这是否有效取决于类型。如果列表元素的类型有界并且n
足够大,xs !! n
会抛出“索引太大”的异常,但算法可能是完全明确定义的。因此转换对Integer
有效,但一般不适用。