Wie kann man über die Stapelsicherheit in Scala Cats / fs2 nachdenken?

13

Hier ist ein Teil des Codes aus der Dokumentation für fs2 . Die Funktion goist rekursiv. Die Frage ist, woher wissen wir, ob es stapelsicher ist und wie man begründet, ob eine Funktion stapelsicher ist?

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)

Wäre es auch stapelsicher, wenn wir govon einer anderen Methode aus aufrufen ?

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
quelle
Nein, nicht genau. Wenn dies der Fall ist, sagen Sie dies bitte, aber es scheint, dass dies nicht der Fall ist. Soweit ich weiß, macht Katzen etwas Magie namens Trampolin, um die Stapelsicherheit zu gewährleisten. Leider kann ich nicht sagen, wann eine Funktion trampoliniert ist und wann nicht.
Lev Denisov
Sie können umschreiben go, um z. B. eine Monad[F]Typklasse zu verwenden. Es gibt eine tailRecMMethode, mit der Sie das Trampolin explizit ausführen können, um sicherzustellen, dass die Funktion stapelsicher ist. Ich könnte mich irren, aber ohne sie verlassen Sie sich darauf, Fdass Sie selbst stapelsicher sind (z. B. wenn es Trampolin intern implementiert), aber Sie wissen nie, wer Ihr definiert F, also sollten Sie dies nicht tun. Wenn Sie keine stapelsichere Garantie haben F, verwenden Sie eine Typklasse, die dies vorsieht, tailRecMda sie gesetzlich stapelsicher ist.
Mateusz Kubuszok
1
Es ist einfach, sich vom Compiler mit @tailrecAnmerkungen für Tail Rec-Funktionen beweisen zu lassen . Für andere Fälle gibt es in der Scala AFAIK keine formellen Garantien. Selbst wenn die Funktion selbst sicher ist, sind die anderen Funktionen, die sie aufruft, möglicherweise nicht: /.
yǝsʞǝla

Antworten:

17

Meine vorherige Antwort hier enthält einige Hintergrundinformationen, die nützlich sein könnten. Die Grundidee ist, dass einige Effekttypen flatMapImplementierungen haben, die eine stapelsichere Rekursion direkt unterstützen. Sie können flatMapAufrufe entweder explizit oder durch Rekursion so tief verschachteln, wie Sie möchten, und Sie werden den Stapel nicht überlaufen lassen.

Bei einigen Effekttypen ist es flatMapaufgrund der Semantik des Effekts nicht möglich , stapelsicher zu sein. In anderen Fällen ist es möglicherweise möglich, einen Stack-Safe zu schreiben flatMap, aber die Implementierer haben möglicherweise aufgrund der Leistung oder anderer Überlegungen entschieden, dies nicht zu tun.

Leider gibt es keine Standardmethode (oder sogar keine konventionelle Methode), um festzustellen, ob die flatMapfür einen bestimmten Typ stapelsicher ist. Cats enthält eine tailRecMOperation, die eine stapelsichere monadische Rekursion für jeden rechtmäßigen monadischen Effekttyp bereitstellen sollte, und manchmal kann ein Blick auf eine tailRecMImplementierung, die als rechtmäßig bekannt ist, einige Hinweise darauf geben, ob a flatMapstapelsicher ist. Im Fall des Pulles sieht aus wie diese :

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

Dies tailRecMist Rekursion nur durch flatMap, und wir wissen , dass Pull‚s MonadInstanz rechtmäßig ist , was ziemlich gut Beweise dafür, dass Pull‘ s flatMapheißt Stapel sicher. Der einzige komplizierende Faktor hierbei ist, dass die Instanz für Pulleine ApplicativeErrorEinschränkung hat F, Pulldie flatMapdies nicht tut, aber in diesem Fall ändert dies nichts.

Die tkImplementierung hier ist also stapelsicher, da flatMapon Pullstapelsicher ist, und das wissen wir aus der Betrachtung der tailRecMImplementierung. (Wenn wir etwas tiefer graben, können wir herausfinden, dass dies flatMapstapelsicher ist, da Pulles sich im Wesentlichen um eine Hülle handelt, für FreeCdie Trampoline verwendet werden .)

Es wäre wahrscheinlich nicht besonders schwer, tkin Bezug auf neu zu schreiben tailRecM, obwohl wir die ansonsten unnötige ApplicativeErrorEinschränkung hinzufügen müssten . Ich vermute, die Autoren der Dokumentation haben sich aus Gründen der Klarheit dafür entschieden, dies nicht zu tun, und weil sie wussten Pull, dass flatMapes in Ordnung ist.


Update: Hier ist eine ziemlich mechanische tailRecMÜbersetzung:

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

Beachten Sie, dass es keine explizite Rekursion gibt.


Die Antwort auf Ihre zweite Frage hängt davon ab, wie die andere Methode aussieht. Im Fall Ihres speziellen Beispiels >>werden jedoch nur mehr flatMapEbenen erstellt, sodass dies in Ordnung sein sollte.

Um Ihre Frage allgemeiner zu beantworten, ist dieses ganze Thema in Scala ein verwirrendes Durcheinander. Sie sollten sich nicht wie oben beschrieben mit Implementierungen befassen müssen, um zu wissen, ob ein Typ eine stapelsichere monadische Rekursion unterstützt oder nicht. Bessere Konventionen in Bezug auf die Dokumentation wären hier eine Hilfe, aber leider machen wir das nicht sehr gut. Sie könnten immer tailRecM"sicher" sein (was Sie F[_]sowieso tun möchten, wenn das generisch ist), aber selbst dann vertrauen Sie darauf, dass die MonadImplementierung rechtmäßig ist.

Zusammenfassend lässt sich sagen, dass es rundum eine schlechte Situation ist. In sensiblen Situationen sollten Sie auf jeden Fall Ihre eigenen Tests schreiben, um zu überprüfen, ob solche Implementierungen stapelsicher sind.

Travis Brown
quelle
Vielen Dank für die Erklärung. Was die Frage betrifft, wenn wir govon einer anderen Methode aus aufrufen , was kann dazu führen, dass der Stapel unsicher wird? Wenn wir vor dem Aufruf einige nicht rekursive Berechnungen durchführen, Pull.output(hd) >> go(tl, n - m)ist das in Ordnung?
Lev Denisov
Ja, das sollte in Ordnung sein (vorausgesetzt, die Berechnung selbst läuft natürlich nicht über den Stapel).
Travis Brown
Welcher Effekttyp wäre beispielsweise für eine monadische Rekursion nicht stapelsicher? Der Fortsetzungstyp?
Bob
@bob Richtig, obwohl Catss ContT‚s flatMap ist eigentlich Stack-safe (über eine DeferEinschränkung des zugrunde liegenden Typs). Ich dachte eher an so etwas wie List, wo das Rekursieren flatMapnicht stapelsicher ist (es hat jedoch ein Gesetz tailRecM).
Travis Brown