我知道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
效果长时间返回空字符串也不会炸毁堆?
答案 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解释器何时使用其内部堆栈的简短讨论:
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
链的长度。flatMaps
,或者链式折叠和链式io.foldM(h1, f1).foldM(h2, f2).foldM(h3, f3)
的混合。如果没有错误,则折叠行为类似于flatMaps
,因此有关ZIO堆栈的分析非常相似。您应该期望ZIO堆栈的最大深度至少为链的长度。flatMaps
和flatMap
之上实现:
foldCauseM
,map
,as
,zip
,zipWith
,<*
,*>
,foldLeft
在foreach
flatMap
,fold
,foldM
,catchSome
,catchAll
在mapError
上实现最后但并非最不重要的:除非您担心ZIO内部堆栈的大小,否则不要担心