让我们假设您有一个程序以某种方式操纵流Stream[Foo]
以产生感兴趣的计算,例如
myFooStream.map(toBar).groupBy(identity).mapValues(_.size)
很可爱,除非您现在必须在myFooStream
上进行其他类型的计算,例如
myFooStream.map(toBar).sum
并且你想以某种方式编写这些计算,这样你就不需要在流上迭代两次了(让我们说因为某种原因迭代流是昂贵的)。
是否有一些Scala-ish方法来处理这个问题?我的问题更抽象地说,我想以某种方式从这些流上的迭代中抽象出这些流的计算。也就是说,最好的情况是,如果我能够以某种方式编写两个方法f: Stream[Foo] => Bar
和g: Stream[Foo] => Baz
并以某种方式组合f
和g
,使得它们在单次迭代中运行流。
是否有一些抽象允许这个?
更新的问题:我做了一点挖掘。 scalaz箭头会对这个问题有帮助吗?
答案 0 :(得分:2)
Streams
通过记忆结果自然会尽可能避免多次生成元素。来自docs:
Stream
类还使用memoization,以便先前计算的值从Stream元素转换为A
类型的具体值。
我们可以看到,通过构造Stream
,每次生成元素时都会打印,并运行多个操作:
val stream = Stream.from(0).map(x => { println(x); x }).take(10) //prints 0
val double = stream.map(_ * 2).take(5).toList //prints 1 through 4
val sum = stream.sum //prints 5 through 9
val sum2 = stream.sum //doesn't print any more
只要您使用val
而不是def
:
只要有东西抓住头部,头部就会抓住尾巴,所以它会继续递归。另一方面,如果头部没有任何东西(例如我们使用
def
来定义Stream
),那么一旦它不再被直接使用,它就会消失。
此备忘录意味着必须谨慎Streams
:
必须谨慎记忆;如果你不小心,你可以很快吃掉大量的记忆。这样做的原因是
Stream
的记忆创建了一个类似于scala.collection.immutable.List
的结构。
当然,如果项目的生成不是昂贵的,但Stream
或memoization的实际遍历是不可用的,因为它太贵了,人们可以始终将foldLeft
与元组一起使用,跟踪多个值:
//Only prints 0-9 once, even if stream is a def
val (sum, double) = stream.foldLeft(0 -> List.empty[Int]) {
case ((sum, list), next) => (sum + next, list :+ (next * 2))
}
如果这是一个足够普通的操作,您甚至可以丰富Stream
来制作一些更常见的操作,例如foldLeft
,reduceLeft
以及其他可用的格式:
implicit class RichStream[T](val stream: Stream[T]) extends AnyVal {
def doubleFoldLeft[A, B](start1: A, start2: B)(f: (A, T) => A, g: (B, T) => B) = stream.foldLeft(start1 -> start2) {
case ((aAcc, bAcc), next) => (f(aAcc, next), g(bAcc, next))
}
}
这将允许您执行以下操作:
val (sum, double) = stream.doubleFoldLeft(0, List.empty[Int])(_ + _, _ :+ _)
答案 1 :(得分:1)
流不会迭代两次:
Stream.continually{println("bob"); 1}.take(4).map(v => v).sum
bob
bob
bob
bob
4
和
val bobs = Stream.continually{println("bob"); 1}.take(4)
val alices = Stream.continually{println("alice"); 2}.take(4)
bobs.zip(alices).map{ case (b, a) => a + b}.sum
bob
bob
bob
bob
alice
alice
alice
alice
12