【发布时间】:2012-11-16 19:00:17
【问题描述】:
我正在尝试编写一个枚举器,用于使用Scalaz 7 的 iteratee 库从java.io.BufferedReader 中逐行读取文件,该库目前只为java.io.Reader 提供一个(非常慢的)枚举器。
我遇到的问题与以下事实有关类型的构造函数,而 Scalaz 7 没有。
我目前的实现
这是我目前拥有的。首先是一些导入和IO 包装器:
import java.io.{ BufferedReader, File, FileReader }
import scalaz._, Scalaz._, effect.IO, iteratee.{ Iteratee => I, _ }
def openFile(f: File) = IO(new BufferedReader(new FileReader(f)))
def readLine(r: BufferedReader) = IO(Option(r.readLine))
def closeReader(r: BufferedReader) = IO(r.close())
还有一个类型别名来清理一下:
type ErrorOr[A] = Either[Throwable, A]
现在是 tryIO 助手,模仿(松散地,可能是错误地)enumerator 中的助手:
def tryIO[A, B](action: IO[B]) = I.iterateeT[A, IO, ErrorOr[B]](
action.catchLeft.map(
r => I.sdone(r, r.fold(_ => I.eofInput, _ => I.emptyInput))
)
)
BufferedReader 本身的枚举器:
def enumBuffered(r: => BufferedReader) = new EnumeratorT[ErrorOr[String], IO] {
lazy val reader = r
def apply[A] = (s: StepT[ErrorOr[String], IO, A]) => s.mapCont(k =>
tryIO(readLine(reader)) flatMap {
case Right(None) => s.pointI
case Right(Some(line)) => k(I.elInput(Right(line))) >>== apply[A]
case Left(e) => k(I.elInput(Left(e)))
}
)
}
最后是一个负责打开和关闭阅读器的枚举器:
def enumFile(f: File) = new EnumeratorT[ErrorOr[String], IO] {
def apply[A] = (s: StepT[ErrorOr[String], IO, A]) => s.mapCont(k =>
tryIO(openFile(f)) flatMap {
case Right(reader) => I.iterateeT(
enumBuffered(reader).apply(s).value.ensuring(closeReader(reader))
)
case Left(e) => k(I.elInput(Left(e)))
}
)
}
现在假设我想将文件中包含至少 25 个'0' 字符的所有行收集到一个列表中。我会写:
val action: IO[ErrorOr[List[String]]] = (
I.consume[ErrorOr[String], IO, List] %=
I.filter(_.fold(_ => true, _.count(_ == '0') >= 25)) &=
enumFile(new File("big.txt"))
).run.map(_.sequence)
在许多方面,这似乎工作得很好:我可以使用unsafePerformIO 开始操作,它会在几分钟内将数千万行和千兆字节的数据分块,存储在恒定的内存中,并且不会破坏堆栈,然后在完成后关闭阅读器。如果我给它一个不存在的文件的名称,它会尽职尽责地将包含在 Left 中的异常返回给我,并且如果在读取时遇到异常,enumBuffered 至少似乎表现得适当。
潜在问题
不过,我对自己的实现有些担忧——尤其是tryIO。例如,假设我尝试编写几个迭代器:
val it = for {
_ <- tryIO[Unit, Unit](IO(println("a")))
_ <- tryIO[Unit, Unit](IO(throw new Exception("!")))
r <- tryIO[Unit, Unit](IO(println("b")))
} yield r
如果我运行它,我会得到以下信息:
scala> it.run.unsafePerformIO()
a
b
res11: ErrorOr[Unit] = Right(())
如果我在 GHCi 中使用 enumerator 尝试相同的操作,结果会更符合我的预期:
...> run $ tryIO (putStrLn "a") >> tryIO (error "!") >> tryIO (putStrLn "b")
a
Left !
我只是看不到在 iteratee 库本身中没有错误状态的情况下获得此行为的方法。
我的问题
我并不声称自己是迭代器方面的专家,但我在几个项目中使用了各种 Haskell 实现,感觉我或多或少地了解了基本概念,并且曾与 Oleg 喝过咖啡。不过,我在这里不知所措。这是在没有错误状态的情况下处理异常的合理方法吗?有没有办法实现tryIO,它的行为更像enumerator 版本?由于我的实现行为不同,是否有某种定时炸弹在等着我?
【问题讨论】:
-
无论如何我不会给你一个好的答案,但我很好奇:你的性能和/或抽象目标是什么?大概你关心一些性能,或者
Reader会很好;并且大概您关心一些抽象,或者您会放弃这种开销并使用更紧凑且可能性能更好的策略(对于简单且不需要组合的情况)。例如,Try(closing(io.Source.fromFile("big.txt")){_.getLines.filter(_.count(_=='0') >= 25)}.toList),closing具有明显的三行定义。 -
@RexKerr:Scalaz 附带的
Reader枚举器的性能非常糟糕——实际上是我在这里实现的枚举器的几十倍。但是我很高兴能够为例如flatMap(enumFile)列出目录中的文件并获取所有这些文件的行的枚举器的枚举器提供便利,而无需担心显式关闭任何资源(iteratee 方法允许)。 -
如果您可以为当前示例提供一个完整的工作要点,我可以尝试将其转换为我在答案中写的内容。
-
@IvanMeredith:谢谢!这是the gist,我已经针对
scalaz-seven分支的当前负责人进行了测试。 -
记录在此my solution。
标签: scala haskell io scalaz iterate