如何在ZIO中实现不使用潜在大量堆空间的循环

时间:2019-12-04 12:39:25

标签: scala zio

我知道ZIO维护着自己的堆栈,即zio.internal.FiberContext#stack,它可以保护递归函数,例如

def getNameFromUser(askForName: UIO[String]): UIO[String] =
  for {
    resp <- askForName
    name <- if (resp.isEmpty) getNameFromUser(askForName) else ZIO.succeed(resp)
  } yield name

来自堆栈溢出。但是,它们仍然占用ZIO解释器堆栈中的空间,这可能导致OutOfMemoryError获得非常深的递归。您将如何从上方重写getNameFromUser函数,以使askForName效果长时间返回空字符串也不会炸毁堆?

2 个答案:

答案 0 :(得分:4)

您正在递归函数中使用循环。基本上,每次调用getNameFromUser时,都将对象分配给堆,因为您在t1上创建的对象需要在t2上创建的对象才能解析,但从t2来的对象需要使用在t3上放置对象以解决广告无限问题。

您应该像使用forever或在Schedule上找到的其他任何方法一样使用ZIO组合器,而不要使用循环

 import zio.Schedule

 val getNameFromUser: RIO[Console, String] = for {
  _    <- putStrLn("Waht is your name")
  name <- zio.console.getStrLn
 } yield name

 val runUntilNotEmpty = Schedule.doWhile[String](_.isEmpty)

 rt.unsafeRun(getNameFromUser.repeat(runUntilNotEmpty))

[EDIT]添加一个不同的示例cuz,您实际需要的只是:

import zio._
import zio.console._
import scala.io.StdIn

object ConsoleEx extends App {

  val getNameFromUser = for {
    _    <- putStrLn("What is your name?")
    name <- getStrLn
    _    <- putStr(s"Hello, $name")
  } yield ()

  override def run(args: List[String]) =
    getNameFromUser.fold(t => {println(t); 1}, _ => 0)

}

但是请注意,如果您在fork in run := true中拥有build.sbt,则还需要按照in the sbt docs的说明添加run / connectInput := true

答案 1 :(得分:0)

从上面重写功能的推荐方法是使用Schedule所建议的适当的toxicafunk,从而导致

def getNameFromUserSchedule(askForName: UIO[String]): UIO[String] =
  askForName.repeat(Schedule.doWhile(_.isEmpty))

这既简洁又可读,并且仅消耗恒定数量的ZIO堆栈帧。

但是,您不必使用Schedule来制作

def getNameFromUser(askForName: UIO[String]): UIO[String] =
  for {
    resp <- askForName
    name <- if (resp.isEmpty) getNameFromUser(askForName) else ZIO.succeed(resp)
  } yield name

消耗恒定数量的ZIO堆栈帧。也可以这样:

def getNameFromUser(askForName: UIO[String]): UIO[String] =
  askForName.flatMap { resp =>
    if (resp.isEmpty) getNameFromUser(askForName) else ZIO.succeed(resp)
  }

此功能看起来像原始版本,其原始格式为

def getNameFromUser(askForName: UIO[String]): UIO[String] =
  askForName.flatMap { resp =>
    if (resp.isEmpty) getNameFromUser(askForName) else ZIO.succeed(resp)
  }.map(identity)

唯一的区别是最后的map(identity)。解释从此函数生成的ZIO值时,解释器必须将identity推入堆栈,计算flatMap,然后应用identity。但是,要计算flatMap,可能会重复相同的过程,迫使解释器将具有循环迭代次数的identities推入堆栈。这有点烦人,但解释器无法知道,它推入堆栈的功能实际上是身份。通过使用better-monadic-for编译器插件,您可以消除它们而无需放弃漂亮的for语法,该插件可以在降低理解力时优化最终的map(identity)

没有map(identity),解释器将执行askForName,然后使用闭包

resp =>
    if (resp.isEmpty) getNameFromUser(askForName) else ZIO.succeed(resp)

获取下一个ZIO值进行解释。此过程可能会重复任意次数,但是解释器堆栈的大小将保持不变。

总结一下,这里是有关ZIO解释器何时使用其内部堆栈的简短讨论:

  1. 计算链接flatMaps时,就像io0.flatMap(f1).flatMap(f2).flatMap(f3)一样。为了评估这样的表达式,解释器将把f3推入堆栈,并查看io0.flatMap(f1).flatMap(f2)。然后它将f2放在堆栈上,并查看io0.flatMap(f1)。最后,f1将被放到栈中,并且对io0进行评估(解释器中有一个优化,此处可能有一个捷径,但这与讨论无关)。在将io0评估为r0之后,f1从堆栈中弹出,并应用于r0的结果,为我们提供了一个新的ZIO值{{1} }。现在将io1 = f1(r0)评估为io1,并从堆栈中弹出r1,以获得下一个ZIO值f2。最后,将io2 = f2(r1)评估为io2,从堆栈中弹出r2以获得f3,并将io3 = f3(r2)解释为io3,这是最终结果的表达。因此,如果您有一种算法,可以通过将r3链接在一起来工作,则应该期望ZIO堆栈的最大深度至少为您的flatMaps链的长度。
  2. 在计算链式折叠时,例如flatMaps,或者链式折叠和链式io.foldM(h1, f1).foldM(h2, f2).foldM(h3, f3)的混合。如果没有错误,则折叠行为类似于flatMaps,因此有关ZIO堆栈的分析非常相似。您应该期望ZIO堆栈的最大深度至少为链的长度。
  3. 应用上述规则时,请记住,有许多组合器直接或间接在flatMapsflatMap之上实现:
    • foldCauseMmapaszipzipWith<**>foldLeftforeach
    • 之上实现
    • flatMapfoldfoldMcatchSomecatchAllmapError上实现

最后但并非最不重要的:除非您担心ZIO内部堆栈的大小,否则不要担心

  • 您正在实现一种算法,其中迭代次数对于仅适度甚至恒定大小的输入数据可能会变得任意大
  • 您正在遍历非常大的数据结构,这些结构不适合内存
  • 用户可以轻松地直接影响堆栈深度(例如,无需通过网络发送大量数据)