List Haskell:列表与流
我注意到,除了固定的时间附加之外,它的行为似乎很像列表。当然,向列表中添加常量时间附加并不太复杂,而且确实如此 在接下来的讨论中,我们假设列表有固定的时间附加,或者我们对它不感兴趣 我的想法是Haskell列表应该简单地实现为流。为避免出现这种情况,我假设以下情况需要保持:List Haskell:列表与流,list,haskell,stream,List,Haskell,Stream,我注意到,除了固定的时间附加之外,它的行为似乎很像列表。当然,向列表中添加常量时间附加并不太复杂,而且确实如此 在接下来的讨论中,我们假设列表有固定的时间附加,或者我们对它不感兴趣 我的想法是Haskell列表应该简单地实现为流。为避免出现这种情况,我假设以下情况需要保持: 在某些情况下,列表优于流和 在某些情况下,流比列表更好 我的问题是:上述两种情况的例子是什么 注意:对于这个问题,请忽略我所讨论的特定实现中容易修复的遗漏。我在这里寻找更多的核心结构差异 其他信息: 我想我在这里得到的部分内
[1..1000000]
,Haskell编译器(比如GHC)是否会:
或者,如果是第(2)种情况,那么我们为什么需要流呢?流的优势在于它们更强大。界面:
data Stream m a = forall s . Stream (s -> m (Step s a)) s Size
data [] a = a : [a] | []
让您完成许多常规列表无法完成的事情。例如:
- 跟踪大小(如未知,最大34,精确到12)
- 执行一元操作以获取下一个元素。列表可以在惰性IO中部分实现这一点,但事实证明,这种技术很容易出错,通常只用于初学者或简单的小脚本
data Stream m a = forall s . Stream (s -> m (Step s a)) s Size
data [] a = a : [a] | []
这是非常简单的,可以很容易地教给一个新的程序员
列表的另一个优点是,您可以简单地对它们进行模式匹配。例如:
getTwo (a : b : _) = Just (a,b)
getTwo _ = Nothing
这对有经验的程序员(我仍然在许多方法中使用列表模式匹配)和还没有学会可用于操作列表的标准高阶函数的初学者都很有用
效率也是列表的另一个潜在优势,因为ghc在列表融合方面花费了大量时间。在许多代码中,从来不会生成中间列表。使用流进行优化可能会困难得多
所以我认为用流交换列表是一个糟糕的选择。目前的情况更好,如果你需要的话,你可以把它们带进来,但是初学者不会被它们的复杂性所困扰,熟练的用户也不必失去模式匹配
编辑:关于[1..1000000]
:
这相当于
enumfromto1000000
,这是一种延迟评估,并且需要进行融合(这使得它非常有效)。例如sum[1..1000000]
在启用优化的情况下不会生成任何列表(并使用恒定内存)。所以,情况(2)是正确的,由于延迟计算,这种情况对流来说不是一个优势。但是如上所述,流比列表有其他优势。当您编写[1..1000000]
时,GHC真正做的是创建一个包含1
和1000000
的对象,描述如何构建感兴趣的列表;那个物体叫做“砰”。该列表仅在满足案例审查者的需要时建立;例如,您可以编写:
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 ""
评估就要结束了
我们需要流的原因有点微妙。原文,大概有最完整的解释。简短的版本是,当管道较长时:
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)
然后观察我们总是可以从列表中删除。收费表
:
toList . S.concatMap foo . S.map bar . S.filter pred . S.break . fromList
…然后奇迹发生了,因为链
S.concatMap foo。美国地图栏。美国过滤器公司。S.break显式构建迭代器,而不是通过内部构建然后立即消除实际列表来隐式构建迭代器。简短回答:列表和流在功能上是无与伦比的。流允许一元操作,但不允许共享,而列表则相反
一个较长的答案:
1) 请参阅@nanothief以了解无法使用列表实现的反例
2) 下面是一个反例,它不能很容易地用流实现
问题是玩具列表示例通常不使用列表的共享功能。代码如下:
foo = map heavyFunction bar
baz = take 5 foo
quux = product foo
对于列表,只需计算一次重函数。使用流计算baz
和qux
而不额外计算heavyFunction
的代码将很难维护。Hm,你为什么说流具有恒定的附加/前置时间?从实现来看,似乎追加n个元素将导致