我如何实现不在 ZIO 中使用可能非常大量的堆空间的循环
How can I implement loops that don't use potentially very large amounts of heapspace in 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 解释器堆栈中的 space,这可能导致 OutOfMemoryError
用于非常深的递归。您将如何重写上面的 getNameFromUser
函数,即使 askForName
影响 returns 空字符串很长时间也不会破坏堆?
您正在递归函数中使用循环。基本上,每次您调用 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)
}
但是请注意,如果您在 build.sbt
中包含 fork in run := true
,那么您还需要按照 in the sbt docs
的说明添加 run / connectInput := true
重写上述函数的推荐方法是使用适当的 Schedule, as suggested by 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 值,io1 = f1(r0)
.现在 io1
被评估为 r1
并且 f2
从堆栈中弹出,以获得下一个 ZIO 值 io2 = f2(r1)
。最后,io2
被求值为r2
,f3
出栈得到io3 = f3(r2)
,io3
被解释为r3
,最终结果的表达。因此,如果您有一个算法,通过将 flatMaps
链接在一起来工作,您应该期望 ZIO 堆栈的最大深度至少是 flatMaps
. 链的长度
- 计算链式折叠时,例如
io.foldM(h1, f1).foldM(h2, f2).foldM(h3, f3)
,或链式折叠和链式折叠的混合 flatMaps
。如果没有错误,folds 的行为类似于 flatMaps
,因此关于 ZIO 堆栈的分析非常相似。您应该期望 ZIO 堆栈的最大深度至少是您的链的长度。
- 在应用上述规则时,请记住,有许多组合器直接或间接地在
flatMap
和 foldCauseM
之上实现:
map
、as
、zip
、zipWith
、<*
、*>
、foldLeft
、foreach
在 flatMap
之上实现
fold
、foldM
、catchSome
、catchAll
、mapError
是在 foldCauseM
之上实现的
最后但并非最不重要的一点:您不必太担心 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 解释器堆栈中的 space,这可能导致 OutOfMemoryError
用于非常深的递归。您将如何重写上面的 getNameFromUser
函数,即使 askForName
影响 returns 空字符串很长时间也不会破坏堆?
您正在递归函数中使用循环。基本上,每次您调用 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)
}
但是请注意,如果您在 build.sbt
中包含 fork in run := true
,那么您还需要按照 in the sbt docs
run / connectInput := true
重写上述函数的推荐方法是使用适当的 Schedule, as suggested by 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 值,io1 = f1(r0)
.现在io1
被评估为r1
并且f2
从堆栈中弹出,以获得下一个 ZIO 值io2 = f2(r1)
。最后,io2
被求值为r2
,f3
出栈得到io3 = f3(r2)
,io3
被解释为r3
,最终结果的表达。因此,如果您有一个算法,通过将flatMaps
链接在一起来工作,您应该期望 ZIO 堆栈的最大深度至少是flatMaps
. 链的长度
- 计算链式折叠时,例如
io.foldM(h1, f1).foldM(h2, f2).foldM(h3, f3)
,或链式折叠和链式折叠的混合flatMaps
。如果没有错误,folds 的行为类似于flatMaps
,因此关于 ZIO 堆栈的分析非常相似。您应该期望 ZIO 堆栈的最大深度至少是您的链的长度。 - 在应用上述规则时,请记住,有许多组合器直接或间接地在
flatMap
和foldCauseM
之上实现:map
、as
、zip
、zipWith
、<*
、*>
、foldLeft
、foreach
在flatMap
之上实现
fold
、foldM
、catchSome
、catchAll
、mapError
是在foldCauseM
之上实现的
最后但并非最不重要的一点:您不必太担心 ZIO 内部堆栈的大小,除非
- 您正在实施一种算法,其中对于中等大小甚至恒定大小的输入数据,迭代次数可能会变得任意大
- 您正在遍历非常大的数据结构,内存无法容纳
- 用户可以毫不费力地直接影响堆栈深度(例如,这意味着无需通过网络向您发送大量数据)