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】:

StreamIterator 之间的主要区别在于后者是可变的和“一次性”,可以这么说,而前者不是。 IteratorStream 具有更好的内存占用,但它可变这一事实可能会带来不便。

以这个经典的素数生成器为例:

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】:

StreamIterator 就像 immutable.Listmutable.List。偏爱不变性可以防止一类错误,有时会以性能为代价。

scalac 本身也不能幸免于这些问题:http://article.gmane.org/gmane.comp.lang.scala.internals/2831

正如 Daniel 指出的那样,偏爱懒惰而不是严格可以简化算法并使它们更容易组合。

【讨论】:

当然,对于那些刚接触惰性的人来说,主要的警告是它会降低代码的可预测性,可能导致海森错误,并且可能对某些算法类别造成严重的性能问题。

以上是关于Scala 中流的用例的主要内容,如果未能解决你的问题,请参考以下文章

Linux文件系统的用例建模

Scala 中的两种柯里化方式;每个的用例是啥?

Scala:Map.flatten 的用例?

pytest 用例编写规则命令行执行用例用例执行的先后顺序

HttpRunner 跳过用例录制生成用例用例分层机制

设计模式 用例图之二