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

1

上記の関数を書き直すための推奨される方法は、 toxicafunkによって提案されているように、適切なScheduleを使用することです。

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)

唯一の違いは finalmap(identity)です。この関数から生成された ZIO 値を解釈する場合、インタープリターは をidentityスタックにプッシュし、 を計算してflatMapから、 を適用する必要がありidentityます。ただし、 を計算するためflatMapに、同じ手順が繰り返される可能性があり、インタープリターidentitiesはループ反復と同じ数をスタックにプッシュする必要があります。これはちょっと厄介ですが、インタプリタは、スタックにプッシュする関数が実際には ID であることを知ることができません。better-monadic-forコンパイラ プラグインforを使用することで、nice 構文を削除せずにそれらを削除できます。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. flatMap上記のルールを適用するときは、 andの上に直接的または間接的に実装される多くのコンビネータがあることに注意してfoldCauseMください。
    • mapaszipzipWith<**>が実装されていfoldLeftますforeachflatMap
    • foldfoldMcatchSomecatchAllmapError上に実装されていますfoldCauseM

最後になりましたが、ZIO の内部スタックのサイズについてあまり心配する必要はありません。

  • 中程度または一定のサイズの入力データに対してのみ反復回数が任意に大きくなる可能性があるアルゴリズムを実装しています
  • メモリに収まらない非常に大きなデータ構造をトラバースしている
  • ユーザーは、ほとんど労力をかけずにスタックの深さに直接影響を与えることができます (つまり、ネットワーク経由で大量のデータを送信する必要はありません)。
于 2019-12-15T23:53:45.960 に答える