【问题标题】:How to use IO with Scalaz7 Iteratees without overflowing the stack?如何在不溢出堆栈的情况下将 IO 与 Scalaz7 Iteratees 一起使用?
【发布时间】:2013-04-14 19:44:57
【问题描述】:

考虑这段代码(取自 here 并修改为使用字节而不是字符行)。

import java.io.{ File, InputStream, BufferedInputStream, FileInputStream }
import scalaz._, Scalaz._, effect._, iteratee.{ Iteratee => I, _ }
import std.list._

object IterateeIOExample {
  type ErrorOr[+A] = EitherT[IO, Throwable, A]

  def openStream(f: File) = IO(new BufferedInputStream(new FileInputStream(f)))
  def readByte(s: InputStream) = IO(Some(s.read()).filter(_ != -1))
  def closeStream(s: InputStream) = IO(s.close())

  def tryIO[A, B](action: IO[B]) = I.iterateeT[A, ErrorOr, B] {
    EitherT(action.catchLeft).map(r => I.sdone(r, I.emptyInput))
  }

  def enumBuffered(r: => BufferedInputStream) = new EnumeratorT[Int, ErrorOr] {
    lazy val reader = r
    def apply[A] = (s: StepT[Int, ErrorOr, A]) => s.mapCont(k =>
      tryIO(readByte(reader)) flatMap {
        case None => s.pointI
        case Some(byte) => k(I.elInput(byte)) >>== apply[A]
      })
  }

  def enumFile(f: File) = new EnumeratorT[Int, ErrorOr] {
    def apply[A] = (s: StepT[Int, ErrorOr, A]) =>
      tryIO(openStream(f)).flatMap(stream => I.iterateeT[Int, ErrorOr, A](
        EitherT(
          enumBuffered(stream).apply(s).value.run.ensuring(closeStream(stream)))))
  }

  def main(args: Array[String]) {
    val action = (
      I.consume[Int, ErrorOr, List] &=
      enumFile(new File(args(0)))).run.run
    println(action.unsafePerformIO())
  }
}

在大小合适的文件 (8kb) 上运行此代码会产生 StackOverflowException。一些搜索发现可以通过使用 Trampoline monad 而不是 IO 来避免异常,但这似乎不是一个很好的解决方案 - 牺牲功能纯度来让程序完全完成。解决这个问题的明显方法是使用 IO 或 Trampoline 作为 Monad Transformer 来包装另一个,但我找不到它们中任何一个的 Transformer 版本的实现,而且我还不足以成为一个函数式编程大师知道如何编写我自己的(了解更多关于 FP 是这个项目的目的之一,但我怀疑创建新的 monad 转换器目前有点超出我的水平)。我想我可以围绕创建、运行和返回我的迭代的结果来包装一个大的 IO 操作,但这感觉更像是一种解决方法而不是解决方案。

大概有些 monad 不能转换为 monad 转换器,所以我想知道是否可以在不丢弃 IO 或溢出堆栈的情况下处理大文件,如果可以,如何?

额外问题:我想不出任何方法让迭代对象在处理过程中发出错误信号,除了让它返回 Either,这使得组合它们变得不那么容易。上面的代码展示了如何使用 EitherT 来处理枚举器中的错误,但是它对迭代器是如何工作的呢?

【问题讨论】:

  • 很好地解释了为什么我需要使用 Trampoline 来避免堆栈溢出,但它没有涵盖如何同时使用 IO 和 Trampoline。
  • IO 已经被蹦床了。
  • I.consume 溢出了吗?溢出:(I.consume[Int, Id, List] &= EnumeratorT.enumStream(Stream.fill(10000)(1))).run
  • 我正在处理的文件很少超过 40kb,所以 8kb 相当不错,是的。不幸的是,我认为我需要一次处理一个字节,或者最好是在非常小的组中。

标签: scala scalaz iterate scalaz7


【解决方案1】:

在您的代码的不同位置创建异常并打印它们的堆栈长度后,我觉得您的代码没有溢出。一切似乎都以恒定的堆栈大小运行。于是我找了其他地方。最终我复制了consume 的实现并添加了一些堆栈深度打印并确认它在那里溢出。

所以这溢出了:

(I.consume[Int, Id, List] &= EnumeratorT.enumStream(Stream.fill(10000)(1))).run

但是,后来我发现不是这样的:

(I.putStrTo[Int](System.out) &= EnumeratorT.enumStream(Stream.fill(10000)(1)))
  .run.unsafePerformIO()

putStrTo 使用 foldM 并且不知何故不会导致溢出。所以我想知道consume是否可以用foldM来实现。我只是从消耗中复制了一些东西并进行了调整,直到它编译完成:

def consume1[E, F[_]:Monad, A[_]:PlusEmpty:Applicative]: IterateeT[E, F, A[E]] = {
  I.foldM[E, F, A[E]](PlusEmpty[A].empty){ (acc: A[E], e: E) =>
    (Applicative[A].point(e) <+> acc).point[F]
  }
}

它奏效了!打印一长串整数。

【讨论】:

  • 似乎consume1 溢出了Scalaz 7.0.3,至少对我而言。如果增加流大小,您会得到相同的结果吗?我正在尝试追踪a potentially related bug——我注意到如果我在Id 上下文中运行会出现堆栈溢出,而如果我在Trampoline 中运行则会出现堆空间错误。但是,对于您的情况,错误在蹦床环境中消失了,这使我怀疑这些问题可能根本不相关......
  • @AaronNovstrup,它仍然适用于 100000 和 scalaz 7.0.3,所以可能你的问题确实不同。
  • 奇怪。我在使用 Scala 2.10.2、Scalaz 7.0.3、OpenJDK 64 位服务器 VM 1.7.0_25 和堆栈大小为 256k。
猜你喜欢
  • 2012-04-23
  • 1970-01-01
  • 2017-01-23
  • 2014-07-29
  • 2012-05-01
  • 2018-11-13
  • 2014-03-14
  • 2014-12-25
  • 2015-09-30
相关资源
最近更新 更多