当您编写[1..1000000]
时,GHC 真正做的是创建一个对象,其中包含1
并1000000
描述了如何构建感兴趣的列表;该对象称为“thunk”。该列表仅在满足案件审查员的需要时才建立;例如,你可以写:
printList [] = putStrLn ""
printList (x:xs) = putStrLn (show x) >> printList xs
main = printList [1..1000000]
评估如下:
main
= { definition of main }
printList [1..1000000]
= { list syntax sugar }
printList (enumFromTo 1 1000000)
= { definition of printList }
case enumFromTo 1 1000000 of
[] -> putStrLn ""
x:xs -> putStrLn (show x) >> printList xs
= { we have a case, so must start evaluating enumFromTo;
I'm going to skip a few steps here involving unfolding
the definition of enumFromTo and doing some pattern
matching }
case 1 : enumFromTo 2 1000000 of
[] -> putStrLn ""
x:xs -> putStrLn (show x) >> printList xs
= { now we know which pattern to choose }
putStrLn (show 1) >> printList (enumFromTo 2 1000000)
然后您会发现它1
已打印到控制台,我们将从顶部附近开始,enumFromTo 2 1000000
而不是enumFromTo 1 1000000
. 最终,你会打印出所有的数字,是时候进行评估了
printList (enumFromTo 1000000 1000000)
= { definition of printList }
case enumFromTo 1000000 1000000 of
[] -> putStrLn ""
x:xs -> putStrLn (show x) >> printList xs
= { skipping steps again to evaluate enumFromTo }
case [] of
[] -> putStrLn ""
x:xs -> putStrLn (show x) >> printList xs
= { now we know which pattern to pick }
putStrLn ""
和评估将完成。
我们需要流的原因有点微妙。原始论文Stream fusion: From lists to streams to nothing可能有最完整的解释。简短的版本是当您有很长的管道时:
concatMap foo . map bar . filter pred . break isSpecial
...如何让编译器编译掉所有中间列表并不是那么明显。您可能会注意到,我们可以将列表视为一种正在迭代的“状态”,并且这些函数中的每一个,而不是遍历列表,只是改变了每次迭代时修改状态的方式。该Stream
类型试图明确这一点,结果是流融合。下面是它的外观:我们首先将所有这些函数转换为流版本:
(toList . S.concatMap foo . fromList) .
(toList . S.map bar . fromList) .
(toList . S.filter pred . fromList) .
(toList . S.break isSpecial . fromList)
然后观察我们总是可以歼灭fromList . toList
:
toList . S.concatMap foo . S.map bar . S.filter pred . S.break . fromList
...然后神奇的事情发生了,因为链S.concatMap foo . S.map bar . S.filter pred . S.break
显式地构建了一个迭代器,而不是通过内部构建隐式构建它,然后立即消除实际列表。