Scala 中流的用例
Posted
技术标签:
【中文标题】Scala 中流的用例【英文标题】:Use-cases for Streams in Scala 【发布时间】:2011-01-07 00:01:15 【问题描述】:在 Scala 中有一个非常类似于迭代器的 Stream 类。主题Difference between Iterator and Stream in Scala? 提供了一些关于两者异同的见解。
了解如何使用流非常简单,但我没有太多常见用例,我会使用流而不是其他工件。
我现在的想法:
如果您需要使用无限级数。但这对我来说似乎不是一个常见的用例,所以它不符合我的标准。 (如果它很常见,请纠正我,我只是有一个盲点) 如果您有一系列数据,其中每个元素都需要计算,但您可能希望重复使用多次。这很弱,因为我可以将它加载到一个列表中,对于大部分开发人员来说,这个列表在概念上更容易理解。 可能存在大量数据或计算量很大的序列,并且您需要的项目很可能不需要访问所有元素。但在这种情况下,迭代器将是一个很好的匹配,除非您需要进行多次搜索,在这种情况下,您也可以使用列表,即使它的效率会稍低。 有一系列复杂的数据需要重用。这里也可以使用一个列表。尽管在这种情况下,这两种情况都同样难以使用,并且 Stream 会更合适,因为并非所有元素都需要加载。但又不是那么常见……是吗?那么我错过了什么重要的用途吗?还是在很大程度上是开发者的偏好?
谢谢
【问题讨论】:
【参考方案1】:Stream
和Iterator
之间的主要区别在于后者是可变的和“一次性”,可以这么说,而前者不是。 Iterator
比 Stream
具有更好的内存占用,但它可变这一事实可能会带来不便。
以这个经典的素数生成器为例:
def primeStream(s: Stream[Int]): Stream[Int] =
Stream.cons(s.head, primeStream(s.tail filter _ % s.head != 0 ))
val primes = primeStream(Stream.from(2))
也可以很容易地用Iterator
编写,但Iterator
不会保留到目前为止计算的素数。
所以,Stream
的一个重要方面是您可以将它传递给其他函数,而无需先复制它,也不必一次又一次地生成它。
对于昂贵的计算/无限列表,这些事情也可以用Iterator
来完成。无限列表实际上非常有用——您只是不知道它,因为您没有它,所以您已经看到了比处理强制有限大小所需的算法更复杂的算法。
【讨论】:
我想补充的另一个区别是Stream
在其头部元素中从不懒惰。 Stream
的头部以评估形式存储。如果需要一个在请求之前不计算任何元素(包括头部)的序列,那么Iterator
是唯一的选择。
除了 head 元素的非惰性之外,它还会评估您要删除的每个元素。例如:"a"#::"b"#::"c"#::"d"#::Stream.empy[String].drop(3)
将评估 "a"、"b"、"c" 和 "d" 。 "d" 因为它变成了头。
质数生成器的有趣简洁示例。有趣的是,如果我在一个简单的 Scala 控制台中创建它,然后请求 4000 个素数(实际上并没有那么多,我有一个替代定义,它可以在不到 2 秒的时间内创建 100K),它会使 Scala 崩溃,并出现“超出 GC 开销限制”错误.【参考方案2】:
除了 Daniel 的回答,请记住 Stream
对于短路评估很有用。例如,假设我有一大堆函数接受String
并返回Option[String]
,并且我想继续执行它们直到其中一个工作:
val stringOps = List(
(s:String) => if (s.length>10) Some(s.length.toString) else None ,
(s:String) => if (s.length==0) Some("empty") else None ,
(s:String) => if (s.indexOf(" ")>=0) Some(s.trim) else None
);
好吧,我当然不想执行 整个 列表,而且List
上没有任何方便的方法说,“将这些视为函数并执行它们,直到其中一个他们返回的不是None
"。该怎么办?也许是这样:
def transform(input: String, ops: List[String=>Option[String]]) =
ops.toStream.map( _(input) ).find(_ isDefined).getOrElse(None)
这需要一个列表并将其视为Stream
(它实际上不评估任何东西),然后定义一个新的Stream
,它是应用函数的结果(但它还没有评估任何东西),然后搜索第一个定义的——在这里,神奇地,它回顾并意识到它必须应用映射,并从原始列表中获取正确的数据——然后将其从 Option[Option[String]]
解包到Option[String]
使用 getOrElse
。
这是一个例子:
scala> transform("This is a really long string",stringOps)
res0: Option[String] = Some(28)
scala> transform("",stringOps)
res1: Option[String] = Some(empty)
scala> transform(" hi ",stringOps)
res2: Option[String] = Some(hi)
scala> transform("no-match",stringOps)
res3: Option[String] = None
但它有效吗?如果我们将println
放入我们的函数中,这样我们就可以判断它们是否被调用,我们得到
val stringOps = List(
(s:String) => println("1"); if (s.length>10) Some(s.length.toString) else None ,
(s:String) => println("2"); if (s.length==0) Some("empty") else None ,
(s:String) => println("3"); if (s.indexOf(" ")>=0) Some(s.trim) else None
);
// (transform is the same)
scala> transform("This is a really long string",stringOps)
1
res0: Option[String] = Some(28)
scala> transform("no-match",stringOps)
1
2
3
res1: Option[String] = None
(这是 Scala 2.8 的情况;不幸的是,2.7 的实现有时会超出 1。请注意,您确实会累积一长串 None
,因为您的失败会累积,但大概是这样与您在此处的真实计算相比,价格便宜。)
【讨论】:
我其实就是这样一个例子,但是这可以通过Iterator
轻松完成,所以我认为这不是重点。
同意。我应该澄清这不是特定于 Stream 的,或者选择了一个使用多次调用同一 Stream 的示例。【参考方案3】:
我可以想象,如果您实时轮询某个设备,Stream 会更方便。
想想一个 GPS 追踪器,它会在您询问时返回实际位置。您无法预先计算 5 分钟后您将到达的位置。您可能只使用它几分钟来实现 OpenStreetMap 中的路径,或者您可能会使用它在沙漠或热带雨林中进行超过六个月的探险。
或数字温度计或其他类型的传感器,只要硬件处于活动状态并打开,就会重复返回新数据 - 日志文件过滤器可能是另一个示例。
【讨论】:
【参考方案4】:Stream
是 Iterator
就像 immutable.List
是 mutable.List
。偏爱不变性可以防止一类错误,有时会以性能为代价。
scalac 本身也不能幸免于这些问题:http://article.gmane.org/gmane.comp.lang.scala.internals/2831
正如 Daniel 指出的那样,偏爱懒惰而不是严格可以简化算法并使它们更容易组合。
【讨论】:
当然,对于那些刚接触惰性的人来说,主要的警告是它会降低代码的可预测性,可能导致海森错误,并且可能对某些算法类别造成严重的性能问题。以上是关于Scala 中流的用例的主要内容,如果未能解决你的问题,请参考以下文章