4

我知道 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效果返回空字符串很长一段时间也不会破坏堆?

4

2 回答 2

5

您正在递归函数中使用循环。基本上,每次您调用时,getNameFromUser您都在将对象分配给堆,堆永远无法释放这些对象,因为您在 t1 上创建的对象需要在 t2 中创建的对象来解析,但来自 t2 的对象需要在 t3 上的对象来解析无止境。

您应该使用 ZIO 组合器而不是循环,就像forever您可以在Schedule上找到的任何其他组合器一样

 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))

[编辑] 添加一个不同的示例因为你真正需要的是:

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.sbtthen 中,您还需要按照 sbt 文档中的run / connectInput := true说明添加

于 2019-12-04T12:52:45.600 回答
1

从上面重写函数的推荐方法是使用适当的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循环迭代一样多的数量推入堆栈。这有点烦人,但解释器不知道它压入堆栈的函数实际上是身份。for您可以通过使用better-monadic-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评估(解释器中有一个优化,可能会在这里采取捷径,但这与讨论无关)。在评估io0to之后r0f1从堆栈中弹出,并应用于 的结果r0,给我们一个新的 ZIO 值,io1 = f1(r0)。现在io1被评估r1f2从堆栈中弹出,以获得下一个 ZIO 值io2 = f2(r1)。最后,io2被评估为r2f3从堆栈中弹出以获取io3 = f3(r2)io3解释为r3表达式的最终结果。因此,如果您有一个通过链接在一起工作的算法,flatMaps您应该期望 ZIO 堆栈的最大深度至少是您的链的长度flatMaps
  2. 在计算链式折叠时,例如io.foldM(h1, f1).foldM(h2, f2).foldM(h3, f3)或链式折叠和链式的混合flatMaps。如果没有错误,折叠行为类似于flatMaps,因此关于 ZIO 堆栈的分析非常相似。您应该期望 ZIO 堆栈的最大深度至少是您的链的长度。
  3. 应用上述规则时,请记住,有许多组合子直接或间接地在flatMapand之上实现foldCauseM
    • map, as, zip, zipWith, <*, *>, foldLeft,foreach是在flatMap
    • fold, foldM, catchSome, catchAll,mapError是在上面实现的foldCauseM

最后但并非最不重要的一点:你不应该太担心 ZIO 内部堆栈的大小,除非

  • 您正在实现一种算法,其中迭代次数可能会变得任意大,仅适用于中等甚至恒定大小的输入数据
  • 您正在遍历非常大的数据结构,这些数据结构不适合内存
  • 用户可以毫不费力地直接影响堆栈深度(例如,这意味着无需通过网络向您发送大量数据)
于 2019-12-15T23:53:45.960 回答