Come ragionare sulla sicurezza dello stack in Scala Cats / fs2?


13

Ecco un pezzo di codice dalla documentazione per fs2 . La funzione goè ricorsiva. La domanda è: come facciamo a sapere se è stack sicuro e come ragionare se qualche funzione è stack sicuro?

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)

Sarebbe anche sicuro per lo stack se chiamiamo goda un altro metodo?

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
}

No, non esattamente. Anche se questo è il caso della ricorsione della coda, per favore, ditelo, ma sembra che non lo sia. Per quanto ne so i gatti fa un po 'di magia chiamata trampolino per garantire la sicurezza dello stack. Sfortunatamente non so dire quando una funzione è calpestata e quando no.
Lev Denisov,

Puoi riscrivere goper usare, ad esempio, la Monad[F]typeclass: esiste un tailRecMmetodo che ti consente di eseguire esplicitamente il trampolino per garantire che la funzione sia impilabile. Potrei sbagliarmi, ma senza di essa fai affidamento sul fatto di Fessere impilabile da solo (ad esempio se implementa il trampolino internamente), ma non sai mai chi definirà il tuo F, quindi non dovresti farlo. Se non si ha la garanzia che Fsia stack-safe, utilizzare una classe di tipo che fornisce tailRecMperché è stack-safe per legge.
Mateusz Kubuszok,

1
È facile lasciare che il compilatore lo provi con l' @tailrecannotazione per le funzioni di registrazione della coda. Per altri casi non vi sono garanzie formali in Scala AFAIK. Anche se la funzione stessa è sicura, le altre funzioni che sta chiamando potrebbero non essere: /.
yǝsʞǝla,

Risposte:


17

La mia risposta precedente qui fornisce alcune informazioni di base che potrebbero essere utili. L'idea di base è che alcuni tipi di effetti hanno flatMapimplementazioni che supportano direttamente la ricorsione dello stack - è possibile nidificare le flatMapchiamate in modo esplicito o attraverso la ricorsione nel modo desiderato e non traboccare lo stack.

Per alcuni tipi di effetti non è possibile flatMapessere stack-safe, a causa della semantica dell'effetto. In altri casi potrebbe essere possibile scrivere uno stack-safe flatMap, ma gli implementatori potrebbero aver deciso di non farlo a causa di prestazioni o altre considerazioni.

Sfortunatamente non esiste un modo standard (o addirittura convenzionale) per sapere se flatMapper un determinato tipo è stack-safe. Cats include tailRecMun'operazione che dovrebbe fornire una ricorsione monadica stack-safe per qualsiasi tipo di effetto monadico lecito, e talvolta guardare tailRecMun'implementazione che è nota per essere legale può fornire alcuni suggerimenti sul fatto che a flatMapsia stack-safe. Nel caso di Pullsembra che questo :

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

Questo tailRecMè solo recursing attraverso flatMap, e sappiamo che Pull's Monadesempio è lecito , che è abbastanza buona evidenza che Pull' s flatMapnon è in pila di sicurezza. L'unico fattore di complicazione qui è che l'istanza per Pullha un ApplicativeErrorvincolo Fche Pull's flatMapnon lo fa, ma in questo caso, che non cambia nulla.

Quindi l' tkimplementazione qui è stack-safe perché flatMapon Pullè stack-safe e lo sappiamo dal punto di vista tailRecMdell'implementazione. (Se scavassimo un po 'più in profondità, potremmo capire che flatMapè stack-safe perché Pullè essenzialmente un involucro per FreeC, che è calpestato .)

Probabilmente non sarebbe tremendamente difficile riscrivere tkin termini di tailRecM, anche se dovremmo aggiungere il ApplicativeErrorvincolo altrimenti inutile . Immagino che gli autori della documentazione abbiano scelto di non farlo per chiarezza e perché sapevano Pullche flatMapva bene.


Aggiornamento: ecco una tailRecMtraduzione abbastanza meccanica :

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

Nota che non c'è ricorsione esplicita.


La risposta alla tua seconda domanda dipende dall'aspetto dell'altro metodo, ma nel caso del tuo esempio specifico, >>risulteranno solo più flatMaplivelli, quindi dovrebbe andare bene.

Per rispondere alla tua domanda più in generale, l'intero argomento è un disordine confuso in Scala. Non dovresti scavare in implementazioni come abbiamo fatto sopra solo per sapere se un tipo supporta la ricorsione monadica stack-safe o no. Convenzioni migliori sulla documentazione sarebbero di aiuto qui, ma sfortunatamente non stiamo facendo un ottimo lavoro. Potresti sempre tailRecMessere "sicuro" (che è ciò che vorrai fare quando F[_]è generico, comunque), ma anche allora ti fidi che l' Monadimplementazione sia lecita.

Per riassumere: è una brutta situazione tutt'intorno e in situazioni delicate dovresti assolutamente scrivere i tuoi test per verificare che implementazioni come questa siano sicure per lo stack.


Grazie per la spiegazione. Per quanto riguarda la domanda quando chiamiamo goda un altro metodo, cosa può renderlo non sicuro? Se eseguiamo alcuni calcoli non ricorsivi prima di chiamare, Pull.output(hd) >> go(tl, n - m)va bene?
Lev Denisov,

Sì, dovrebbe andare bene (supponendo che il calcolo stesso non trabocchi nello stack, ovviamente).
Travis Brown,

Quale tipo di effetto, ad esempio, non sarebbe sicuro per lo stack per la ricorsione monadica? Il tipo di continuazione?
bob

@bob Destra, anche se I gatti di ContTs' flatMap è in realtà pila di sicurezza (tramite un Defervincolo sul tipo sottostante). Stavo pensando più a qualcosa del genere List, in cui il ricorrere flatMapnon è sicuro per lo stack (ma ha un valore legale tailRecM).
Travis Brown,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.