有时,我发现自己希望在Scala 中的无限流上执行嵌套迭代,以获得理解,但是指定循环终止条件可能有点棘手。有更好的方法来做这种事吗?
我想到的用例是,我不一定预先知道我正在迭代的每个无限流需要多少个元素(但很明显,我知道它不会是一个无穷多的数字)。假设每个流的终止条件可能以某种复杂的方式依赖于for表达式中其他元素的值。
最初的想法是尝试将流终止条件写为if filter子句在for表达式中,但是当在嵌套的无限流上循环时会遇到麻烦,因为在第一个无限流上没有短路的方法,这最终会导致OutOfMemoryError。考虑到 for 表达式如何映射到 map 、flatMap和withFilter方法调用,我理解为什么会这样--我的问题是,做这种事情是否有更好的成语(可能根本不涉及forE 215理解)。
为了给出一个有点做作的例子来说明刚才描述的问题,请考虑下面(非常天真的)代码来生成数字1和2的所有配对:
val pairs = for {
i <- Stream.from(1)
if i < 3
j <- Stream.from(1)
if j < 3
}
yield (i, j)
pairs.take(2).toList
// result: List[(Int, Int)] = List((1,1), (1,2))
pairs.take(4).toList
// 'hoped for' result: List[(Int, Int)] = List((1,1), (1,2), (2,1), (2,2))
// actual result:
// java.lang.OutOfMemoryError: Java heap space
// at scala.collection.immutable.Stream$.from(Stream.scala:1105)显然,在这个简单的示例中,通过将if过滤器移动到对原始流的takeWhile方法调用中可以很容易地避免问题,如下所示:
val pairs = for {
i <- Stream.from(1).takeWhile(_ < 3)
j <- Stream.from(1).takeWhile(_ < 3)
}
yield (i, j)但是为了这个问题的目的,想象一个更复杂的用例,在这个用例中,流终止条件不能很容易地移动到流表达式本身。
发布于 2012-12-26 17:31:08
我已经采纳了Petr的建议,想出了一个我认为更通用的解决方案,因为它不限制的位置,如果在for理解中过滤(尽管它有更多的语法开销)。
这个想法再次将底层流封装在包装对象中,该对象不需要修改就委托flatMap、map和filter方法,但首先对底层流应用takeWhile调用,并使用!isTruncated谓词,其中isTruncated是属于包装器对象的字段。在任何时候对包装器对象调用truncate将翻转isTruncated标志,并有效地结束流上的进一步迭代。这在很大程度上依赖于这样一个事实:对底层流的takeWhile调用是延迟计算的,因此在迭代的后期阶段执行的代码有可能影响其行为。
缺点是您必须保留对您希望能够截断中间迭代的流的引用,方法是将|| s.truncate附加到过滤器表达式(其中s是对包装流的引用)。您还需要确保在流中的每次新迭代之前对包装器对象(或使用新的包装器对象)调用reset,除非您知道每次重复迭代的行为都是相同的。
import scala.collection._
import scala.collection.generic._
class TruncatableStream[A]( private val underlying: Stream[A]) {
private var isTruncated = false;
private var active = underlying.takeWhile(a => !isTruncated)
def flatMap[B, That](f: (A) => GenTraversableOnce[B])(implicit bf: CanBuildFrom[Stream[A], B, That]): That = active.flatMap(f);
def map[B, That](f: (A) => B)(implicit bf: CanBuildFrom[Stream[A], B, That]): That = active.map(f);
def filter(p: A => Boolean): Stream[A] = active.filter(p);
def truncate() = {
isTruncated = true
false
}
def reset() = {
isTruncated = false
active = underlying.takeWhile(a => !isTruncated)
}
}
val s1 = new TruncatableStream(Stream.from(1))
val s2 = new TruncatableStream(Stream.from(1))
val pairs = for {
i <- s1
// reset the nested iteration at the start of each outer iteration loop
// (not strictly required here as the repeat iterations are all identical)
// alternatively, could just write: s2 = new TruncatableStream(Stream.from(1))
_ = _s2.reset()
j <- s2
if i < 3 || s1.truncate
if j < 3 || s2.truncate
}
yield (i, j)
pairs.take(2).toList // res1: List[(Int, Int)] = List((1,1), (1,2))
pairs.take(4).toList // res2: List[(Int, Int)] = List((1,1), (1,2), (2,1), (2,2))毫无疑问,这是可以改进的,但这似乎是一个合理的解决办法。
发布于 2012-12-25 16:32:30
一种可能的方法是将Stream封装到您自己的类中,这个类处理filter的方式不同,在本例中是takeWhile。
import scala.collection._
import scala.collection.generic._
class MyStream[+A]( val underlying: Stream[A] ) {
def flatMap[B, That](f: (A) => GenTraversableOnce[B])(implicit bf: CanBuildFrom[Stream[A], B, That]): That = underlying.flatMap(f);
def map[B, That](f: (A) ⇒ B)(implicit bf: CanBuildFrom[Stream[A], B, That]): That = underlying.map(f);
def filter(p: A => Boolean): Stream[A] = underlying.takeWhile(p);
// ^^^^^^^^^^^^^^^^^^^^^^^^
}
object MyStream extends App {
val pairs = for {
i <- new MyStream(Stream.from(1))
if i < 3
j <- new MyStream(Stream.from(1))
if j < 3
} yield (i, j);
print(pairs.toList);
}这个打印List((1,1), (1,2), (2,1), (2,2))。
https://stackoverflow.com/questions/14026617
复制相似问题