Haskell:Lists vs Streams

时间:2012-06-08 02:42:51

标签: list haskell stream

我注意到streams似乎与列表非常相似,除非有恒定的时间追加。当然,添加常量时间附加到列表并不是太复杂,而DList正是这样做的。

让我们假设在剩下的讨论中,任何一个列表都有不变的时间附加,或者我们根本就不感兴趣。

我的想法是Haskell列表应该简单地实现为流。对于这种情况并非如此,我认为以下内容需要保留:

  1. 在某些情况下,列表优于流 AND
  2. 有些情况下,流比列表更好。
  3. 我的问题是:上述两种情况的例子是什么?

    注意:出于这个问题的目的,请忽略我讨论过的特定实现中容易修复的遗漏。我在这里寻找核心结构差异。

    其他信息:

    我想我在这里得到的部分内容是说如果我们写[1..1000000],Haskell编译器(比如GHC)会这样做:

    1. 列出
    2. 创建一个包含两个整数的对象:1和1000000,它完整地描述了该列表。
    3. 如果是这种情况(1),为什么这样做,因为创建中间列表似乎是不必要的性能损失?

      或者如果是这种情况(2),那么为什么我们需要流?

3 个答案:

答案 0 :(得分:16)

当你写[1..1000000]时,GHC真正做的是创建一个包含11000000的对象,描述如何构建感兴趣的列表;该对象被称为" 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 at all可能有最完整的解释。简短的版本是当你有一个长管道时:

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显式构建了一个迭代器,而不是通过内部构建隐式构建它,然后立即消灭实际列表。

答案 1 :(得分:7)

流的优势在于它们更强大。界面:

data Stream m a = forall s . Stream (s -> m (Step s a)) s Size   

可以让你做许多普通列表无法做到的事情。例如:

  • 跟踪大小(例如Unknown,Max 34,Exact 12)
  • 执行monadic动作以获取下一个元素。列表可以通过惰性IO部分执行此操作,但该技术已证明容易出错,通常仅供初学者使用,或用于简单的小脚本。

但是,与列表相比,它们有很大的缺点 - 复杂性!对于初学者程序员来说,要理解流,你必须在存在类型和monadic动作之上。如果要使用基本列表类型来学习那两个复杂的主题,那么学习haskell会非常困难。

将其与具有接口的列表进行比较:

data [] a = a : [a] | []

这非常简单,可以轻松地向新程序员讲授。

列表的另一个优点是你可以简单地匹配它们。例如:

getTwo (a : b : _) = Just (a,b)
getTwo _ = Nothing

这对有经验的程序员(我仍然在许多方法中使用列表模式匹配)以及尚未学习可用于操作列表的标准高阶函数的初学者程序员都很有用。

效率也是列表的另一个潜在优势,因为ghc花了很多时间研究列表融合。在很多代码中,从不生成中间列表。使用流进行优化可能要困难得多。

所以我认为用Streams交换列表是一个糟糕的选择。目前的情况更好,如果你需要它们你可以把它们带进来,但是初学者不会受到他们的复杂性的困扰,熟练的用户也不必失去模式匹配。

编辑:关于[1..1000000]

这相当于enumFromTo 1 1000000,它被懒惰地评估,并且融合(这使它非常有效)。例如,sum [1..1000000]在启用优化时不会生成任何列表(并使用常量内存)。因此情况(2)是正确的,由于惰性评估,这种情况对于流不是优势。如上所述,流比列表具有其他优势。

答案 2 :(得分:6)

简短回答:列表和流是无与伦比的力量。 Streams允许monadic操作但不允许共享,而列表则相反。

更长的答案:

1)请参阅@nanothief,了解一个无法用列表实现的反例 2)下面是一个不能用流轻易实现的反例

问题是玩具列表示例通常不使用列表的共享功能。这是代码:

foo = map heavyFunction bar
baz = take 5 foo
quux = product foo

使用列表只能计算一次重函数。使用流计算bazquux而无需额外计算heavyFunction的代码将很难维护。