Scala中使用惰性求值或融合的迭代对象?

Scala中使用惰性求值或融合的迭代对象?,scala,scalaz,iterate,Scala,Scalaz,Iterate,我听说迭代者是懒惰的,但他们到底有多懒惰呢?或者,是否可以将迭代对象与后处理函数融合,从而不必构建中间数据结构 例如,在我的迭代对象中,我是否可以从java.io.BufferedReader构建一个100万项流[Option[String]],然后以组合方式过滤掉无流,而不需要将整个流保存在内存中?同时保证我不会把事情搞砸?或者类似的东西-它不必使用流 我目前正在使用Scalaz 6,但如果其他迭代实现能够以更好的方式实现这一点,我很想知道 请提供完整的解决方案,包括关闭BufferedRea

我听说迭代者是懒惰的,但他们到底有多懒惰呢?或者,是否可以将迭代对象与后处理函数融合,从而不必构建中间数据结构

例如,在我的迭代对象中,我是否可以从
java.io.BufferedReader
构建一个100万项
流[Option[String]]
,然后以组合方式过滤掉
流,而不需要将整个流保存在内存中?同时保证我不会把事情搞砸?或者类似的东西-它不必使用

我目前正在使用Scalaz 6,但如果其他迭代实现能够以更好的方式实现这一点,我很想知道


请提供完整的解决方案,包括关闭
BufferedReader
并调用
unsafePerformIO
,如果适用。

我应该读得更深入一点。。。这正是枚举的目的。枚举在Scalaz 7和Play 2中定义,但在Scalaz 6中没有定义


枚举用于“垂直”组合(在“垂直集成行业”的意义上),而普通迭代器以“水平”方式一元组合。

下面是一个使用Scalaz 7库的快速迭代器示例,它演示了您感兴趣的属性:恒定内存和堆栈使用

问题 首先,假设我们有一个大的文本文件,每行有一个十进制数字字符串,我们希望找到所有至少包含二十个零的行。我们可以生成如下示例数据:

val w = new java.io.PrintWriter("numbers.txt")
val r = new scala.util.Random(0)

(1 to 1000000).foreach(_ =>
  w.println((1 to 100).map(_ => r.nextInt(10)).mkString)
)

w.close()
现在我们有了一个名为
numbers.txt
的文件。让我们用一个
BufferedReader
打开它:

val reader = new java.io.BufferedReader(new java.io.FileReader("numbers.txt"))
它并没有太大(~97兆字节),但它足够大,我们可以很容易地看到,在我们处理它的过程中,我们的内存使用是否保持不变

设置我们的枚举器 首先,对于一些进口产品:

import scalaz._, Scalaz._, effect.IO, iteratee.{ Iteratee => I }
和一个枚举器(请注意,为了方便起见,我将
IoExceptionOr
s更改为
选项
s):

Scalaz7目前没有提供一种很好的方法来枚举文件的行,因此我们一次只对文件进行一个字符的分块。这当然会非常慢,但我不想在这里担心,因为这个演示的目标是展示我们可以在恒定内存中处理这个大的ish文件,而不会破坏堆栈。本答案的最后一部分给出了一种性能更好的方法,但在这里我们将仅对换行符进行拆分:

val split = I.splitOn[Option[Char], List, IO](_.cata(_ != '\n', false))
如果
splitOn
采用了一个谓词来指定不拆分的位置这一事实让您感到困惑,那么您并不孤单
split
是我们的第一个枚举示例。我们将继续并在其中包装我们的枚举器:

val lines = split.run(enum).map(_.sequence.map(_.mkString))
现在我们在
IO
monad中有了一个
Option[String]
s的枚举数

使用枚举筛选文件 接下来,对于谓词,请记住我们说过,我们希望行中至少有20个零:

val pred = (_: String).count(_ == '0') >= 20
我们可以将其转换为过滤枚举,并将枚举数包装为:

val filtered = I.filter[Option[String], IO](_.cata(pred, true)).run(lines)
我们将设置一个简单的操作,只打印通过此过滤器的所有内容:

val printAction = (I.putStrTo[Option[String]](System.out) &= filtered).run
当然,我们还没有读到任何东西。为此,我们使用
unsafePerformIO

printAction.unsafePerformIO()
现在我们可以看到
一些(“0946943140969200621607610…”)在内存使用保持不变的情况下缓慢滚动。它的速度很慢,错误处理和输出有点笨拙,但对于大约九行代码来说还不错

从迭代对象获取输出 这就是foreach的用法。我们还可以创建一个更像折叠的iteratee,例如收集通过过滤器的元素并在列表中返回它们。只需重复以上所有操作,直到出现
printAction
定义,然后编写以下内容:

val gatherAction = (I.consume[Option[String], IO, List] &= filtered).run
开始行动:

val xs: Option[List[String]] = gatherAction.unsafePerformIO().sequence
现在去喝杯咖啡(可能需要很远的地方)。当你回来的时候,你要么有一个
None
(如果是
IOException
),要么有一个
Some
,包含1943个字符串的列表

自动关闭文件的完整(更快)示例 为了回答您关于关闭阅读器的问题,这里有一个完整的工作示例,大致相当于上面的第二个程序,但是有一个负责打开和关闭阅读器的枚举器。它也快得多,因为它读的是行,而不是字符。首先是导入和两个助手方法:

import java.io.{ BufferedReader, File, FileReader }
import scalaz._, Scalaz._, effect._, iteratee.{ Iteratee => I, _ }

def tryIO[A, B](action: IO[B]) = I.iterateeT[A, IO, Either[Throwable, B]](
  action.catchLeft.map(
    r => I.sdone(r, r.fold(_ => I.eofInput, _ => I.emptyInput))
  )
)

def enumBuffered(r: => BufferedReader) =
  new EnumeratorT[Either[Throwable, String], IO] {
    lazy val reader = r
    def apply[A] = (s: StepT[Either[Throwable, String], IO, A]) => s.mapCont(
      k =>
        tryIO(IO(reader.readLine())).flatMap {
          case Right(null) => s.pointI
          case Right(line) => k(I.elInput(Right(line))) >>== apply[A]
          case e => k(I.elInput(e))
        }
    )
  }
现在是普查员:

def enumFile(f: File): EnumeratorT[Either[Throwable, String], IO] =
  new EnumeratorT[Either[Throwable, String], IO] {
    def apply[A] = (s: StepT[Either[Throwable, String], IO, A]) => s.mapCont(
      k =>
        tryIO(IO(new BufferedReader(new FileReader(f)))).flatMap {
          case Right(reader) => I.iterateeT(
            enumBuffered(reader).apply(s).value.ensuring(IO(reader.close()))
          )
          case Left(e) => k(I.elInput(Left(e)))
        }
      )
  }
我们准备好了:

val action = (
  I.consume[Either[Throwable, String], IO, List] %=
  I.filter(_.fold(_ => true, _.count(_ == '0') >= 20)) &=
  enumFile(new File("numbers.txt"))
).run

现在,处理完成后,读卡器将关闭。

BufferedReader是否会被上述代码关闭?我看不出什么会关闭它。你是对的,你必须在这里自己关闭它,但这只是因为Scalaz 7中当前用于在
IO
中处理迭代器的实用程序非常简单,编写一个负责打开和关闭文件的枚举器并不难。当我有几分钟的时间时,我将发布一个示例。
val action = (
  I.consume[Either[Throwable, String], IO, List] %=
  I.filter(_.fold(_ => true, _.count(_ == '0') >= 20)) &=
  enumFile(new File("numbers.txt"))
).run