Scala Cats / fs2でスタックの安全性を推論する方法は?


13

以下はfs2のドキュメントからのコードの一部です。関数goは再帰的です。問題は、それがスタックセーフであるかどうかをどのように知るか、および関数がスタックセーフかどうかをどのように推論するかです。

import fs2._
// import fs2._

def tk[F[_],O](n: Long): Pipe[F,O,O] = {
  def go(s: Stream[F,O], n: Long): Pull[F,O,Unit] = {
    s.pull.uncons.flatMap {
      case Some((hd,tl)) =>
        hd.size match {
          case m if m <= n => Pull.output(hd) >> go(tl, n - m)
          case m => Pull.output(hd.take(n.toInt)) >> Pull.done
        }
      case None => Pull.done
    }
  }
  in => go(in,n).stream
}
// tk: [F[_], O](n: Long)fs2.Pipe[F,O,O]

Stream(1,2,3,4).through(tk(2)).toList
// res33: List[Int] = List(1, 2)

go別のメソッドから呼び出した場合もスタックセーフになりますか?

def tk[F[_],O](n: Long): Pipe[F,O,O] = {
  def go(s: Stream[F,O], n: Long): Pull[F,O,Unit] = {
    s.pull.uncons.flatMap {
      case Some((hd,tl)) =>
        hd.size match {
          case m if m <= n => otherMethod(...)
          case m => Pull.output(hd.take(n.toInt)) >> Pull.done
        }
      case None => Pull.done
    }
  }

  def otherMethod(...) = {
    Pull.output(hd) >> go(tl, n - m)
  }

  in => go(in,n).stream
}

いいえ、正確ではありません。これが末尾再帰の場合であると教えてくださいが、そうではないようです。私が知る限り、猫はスタックの安全を確保するためにトランポリンと呼ばれる魔法をかけます。残念ながら、関数がいつトランポリンされるか、いつそうでないかはわかりません。
Lev Denisov

typeclass goを使用するように書き換えることができます。関数がスタックセーフであることを保証するためにトランポリンを明示的に実行できるメソッドがあります。私は間違っているかもしれませんが、それなしではそれ自体がスタックセーフであることに依存しています(たとえば、トランポリンを内部で実装している場合)、誰がを定義するかわからないので、これを行うべきではありません。スタックセーフである保証がない場合は、法律によりスタックセーフであるため、が提供する型クラスを使用してください。Monad[F]tailRecMFFFtailRecM
Mateusz Kubuszok

1
コンパイラ@tailrecにtail rec関数の注釈を付けて簡単に証明させることができます。その他の場合、Scala AFAIKには正式な保証はありません。関数自体が安全であっても、それが呼び出している他の関数は:/ではない場合があります。
yǝsʞǝla

回答:


17

ここでの私の以前の答えは、役に立つかもしれないいくつかの背景情報を提供します。基本的な考え方は、一部のエフェクトタイプにはflatMapスタックセーフな再帰を直接サポートする実装があるということです。flatMap明示的に、または再帰を通じて必要なだけ深く呼び出しをネストでき、スタックがオーバーフローしないようにします。

一部のエフェクトタイプでflatMapは、エフェクトのセマンティクスのため、スタックセーフにすることはできません。他の場合では、スタックセーフを作成するflatMapことは可能ですが、パフォーマンスやその他の考慮事項のために、実装者がそうしないことを決定した可能性があります。

残念ながらflatMap、特定の型のがスタックセーフであるかどうかを確認する標準的な(または従来の)方法はありません。Catsには、tailRecM合法的なモナディック効果タイプに対してスタックセーフなモナディック再帰を提供する操作が含まtailRecMれています。合法であることがわかっている実装を見ると、flatMapスタックが安全かどうかについてのヒントが得られる場合があります。この場合、Pull次のようになります

def tailRecM[A, B](a: A)(f: A => Pull[F, O, Either[A, B]]) =
  f(a).flatMap {
    case Left(a)  => tailRecM(a)(f)
    case Right(b) => Pull.pure(b)
  }

これはtailRecMちょうどを通じて再帰されflatMap、私たちは知っていることPullMonadインスタンスが合法であることをかなり良い証拠である、Pulls 'はflatMapスタック・安全です。ここで係数を複雑一つはのインスタンスがあることをPull持っているApplicativeError上の制約FというPullのはflatMapしませんが、何も変更されませんこの場合は。

したがって、on はスタックtkセーフであるためflatMap、ここでの実装はスタックPullセーフであり、そのtailRecM実装を見るとわかります。(少し深く掘ると、本質的にはトランポリン化されたのラッパーであるため、flatMapスタックセーフであることPullがわかります。)FreeC

他の方法では不要な制約を追加する必要がありますtktailRecM、に関して書き換えることはそれほど難しくありませんApplicativeError。私は、明確にするためにそれを行うにはないことを選んだ、と彼らは知っていたので、ドキュメントの作成者を推測しているPullのはflatMap結構です。


更新:これはかなり機械的なtailRecM翻訳です:

import cats.ApplicativeError
import fs2._

def tk[F[_], O](n: Long)(implicit F: ApplicativeError[F, Throwable]): Pipe[F, O, O] =
  in => Pull.syncInstance[F, O].tailRecM((in, n)) {
    case (s, n) => s.pull.uncons.flatMap {
      case Some((hd, tl)) =>
        hd.size match {
          case m if m <= n => Pull.output(hd).as(Left((tl, n - m)))
          case m => Pull.output(hd.take(n.toInt)).as(Right(()))
        }
      case None => Pull.pure(Right(()))
    }
  }.stream

明示的な再帰はないことに注意してください。


2番目の質問への答えは、他の方法がどのように見えるかに依存しますが、特定の例の場合は>>、より多くのflatMapレイヤーになるだけなので、問題ないはずです。

より一般的にあなたの質問に取り組むために、このトピック全体はScalaで混乱を招く混乱です。型がスタックセーフモナディック再帰をサポートしているかどうかを知るためだけに、上記のような実装を掘り下げる必要はありません。ドキュメンテーションに関するより良い規則は、ここでの助けになるでしょうが、残念ながら、私たちはそれについて非常に良い仕事をしていません。常にtailRecM「安全」に使用できます(F[_]とにかく、ジェネリックの場合に実行したいことです)が、それでもMonad実装が合法であると信頼していることになります。

要約すると、これはすべての状況で悪い状況であり、デリケートな状況では、このような実装がスタックセーフであることを確認する独自のテストを必ず作成する必要があります。


ご説明ありがとうございます。go別のメソッドから呼び出すときの質問に関して、スタックが安全でなくなる原因は何ですか?呼び出す前に非再帰的な計算を行う場合は問題ありませんか Pull.output(hd) >> go(tl, n - m)
Lev Denisov

はい、それで問題ありません(もちろん、計算自体がスタックをオーバーフローしないことが前提です)。
Travis Brown、

たとえば、モナディック再帰ではスタックセーフにならないエフェクトタイプはどれですか。継続タイプ?
ボブ

猫さんが、右を@bob ContTさんがflatMap ある(を経由して、実際にスタック・安全Defer基本となるタイプの制約)。私はのようなものをもっと考えていましたList。そこでは、再帰flatMapはスタックセーフではありません(tailRecMただし、合法的です)。
Travis Brown、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.