Bagaimana alasan tentang keamanan tumpukan di Scala Cats / fs2?

13

Berikut adalah sepotong kode dari dokumentasi untuk fs2 . Fungsi goini bersifat rekursif. Pertanyaannya adalah bagaimana kita tahu apakah itu stack safe dan bagaimana alasannya jika ada fungsi stack safe?

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)

Apakah ini juga akan menjadi tumpukan aman jika kita memanggil godari metode lain?

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
sumber
Tidak, tidak juga. Meskipun jika ini adalah kasus rekursi ekor tolong katakan demikian, tetapi tampaknya tidak. Sejauh yang saya tahu kucing melakukan sihir yang disebut trampolining untuk memastikan keamanan tumpukan. Sayangnya saya tidak tahu kapan suatu fungsi diinjak-injak dan kapan tidak.
Lev Denisov
Anda dapat menulis ulang gountuk menggunakan mis. Monad[F]Typeclass - ada tailRecMmetode yang memungkinkan Anda untuk melakukan trampolin secara eksplisit untuk menjamin bahwa fungsi tersebut akan menjadi stack safe. Saya mungkin salah tetapi tanpa itu Anda mengandalkan Ftumpukan aman sendiri (misalnya jika itu menerapkan trampolin secara internal), tetapi Anda tidak pernah tahu siapa yang akan menentukan Anda F, jadi Anda tidak harus melakukan ini. Jika Anda tidak memiliki jaminan bahwa itu Fadalah stack safe, gunakan kelas tipe yang menyediakan tailRecMkarena itu adalah stack-safe oleh hukum.
Mateusz Kubuszok
1
Sangat mudah untuk membiarkan kompiler membuktikannya dengan @tailrecanotasi untuk fungsi tail rec. Untuk kasus lain tidak ada jaminan formal di Scala AFAIK. Bahkan jika fungsi itu sendiri aman, fungsi lain yang dihubungi mungkin tidak: /.
yǝsʞǝla

Jawaban:

17

Jawaban saya sebelumnya di sini memberikan beberapa informasi latar belakang yang mungkin berguna. Ide dasarnya adalah bahwa beberapa tipe efek memiliki flatMapimplementasi yang mendukung rekursi aman-stack secara langsung — Anda dapat membuat sarang flatMappanggilan secara eksplisit atau melalui rekursi sedalam yang Anda inginkan dan Anda tidak akan meluap tumpukan.

Untuk beberapa tipe efek, tidak mungkin flatMapmenjadi stack-safe, karena efek semantiknya. Dalam kasus lain dimungkinkan untuk menulis stack-safe flatMap, tetapi pelaksana mungkin memutuskan untuk tidak melakukannya karena kinerja atau pertimbangan lain.

Sayangnya tidak ada standar (atau bahkan konvensional) cara untuk mengetahui apakah flatMaptipe yang diberikan adalah stack-safe. Kucing memang menyertakan tailRecMoperasi yang harus menyediakan rekursi monadik yang aman untuk semua jenis efek monadat yang sah, dan terkadang melihat tailRecMimplementasi yang dikenal sah dapat memberikan beberapa petunjuk tentang apakah flatMapaman-stack. Jika Pullterlihat seperti ini :

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

Ini tailRecMhanya recursing melalui flatMap, dan kita tahu bahwa Pull's Monadcontoh adalah halal , yang merupakan bukti yang cukup bagus bahwa Pull' s flatMapyaitu tumpukan-aman. Satu faktor rumit di sini adalah bahwa contoh untuk Pullmemiliki ApplicativeErrorkendala pada Fyang Pull's flatMaptidak, tetapi dalam kasus ini yang tidak berubah apa-apa.

Jadi tkimplementasinya di sini adalah stack-safe karena flatMapon Pull-stack-safe, dan kita tahu dari melihat tailRecMimplementasinya. (Jika kita menggali sedikit lebih dalam, kita bisa mengetahui bahwa flatMapitu aman-tumpukan karena Pullpada dasarnya adalah pembungkus FreeC, yang diinjak - injak .)

Mungkin tidak akan terlalu sulit untuk menulis ulang tkdalam hal tailRecM, meskipun kita harus menambahkan ApplicativeErrorkendala yang tidak perlu . Saya menduga penulis dokumentasi memilih untuk tidak melakukan itu untuk kejelasan, dan karena mereka tahu Pull's flatMapbaik-baik saja.


Pembaruan: inilah terjemahan yang cukup mekanis 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

Perhatikan bahwa tidak ada rekursi eksplisit.


Jawaban untuk pertanyaan kedua Anda tergantung pada seperti apa metode yang lain, tetapi dalam kasus contoh khusus Anda, >>hanya akan menghasilkan lebih banyak flatMaplapisan, jadi itu akan baik-baik saja.

Untuk menjawab pertanyaan Anda secara lebih umum, seluruh topik ini adalah kekacauan yang membingungkan di Scala. Anda tidak harus menggali implementasi seperti yang kami lakukan di atas hanya untuk mengetahui apakah suatu tipe mendukung rekursi monadik stack-safe atau tidak. Konvensi yang lebih baik seputar dokumentasi akan sangat membantu di sini, tetapi sayangnya kami tidak melakukan pekerjaan dengan sangat baik. Anda selalu bisa menggunakan tailRecMuntuk menjadi "aman" (yang adalah apa yang ingin Anda lakukan ketika F[_]generik, toh), tetapi bahkan kemudian Anda percaya bahwa Monadpenerapannya sah.

Singkatnya: ini adalah situasi yang buruk di sekitar, dan dalam situasi sensitif Anda harus menulis tes Anda sendiri untuk memverifikasi bahwa implementasi seperti ini aman untuk digunakan.

Travis Brown
sumber
Terima kasih atas penjelasannya. Mengenai pertanyaan ketika kita menelepon godari metode lain, apa yang bisa membuatnya tidak aman? Jika kita melakukan perhitungan non-rekursif sebelum menelepon, Pull.output(hd) >> go(tl, n - m)apakah itu boleh?
Lev Denisov
Ya, itu seharusnya baik-baik saja (dengan asumsi perhitungan itu sendiri tidak meluap tumpukan, tentu saja).
Travis Brown
Jenis efek mana, misalnya, yang tidak aman untuk rekursi monadik? Jenis kelanjutan?
bob
@ Bob kanan, meskipun Kucing ini ContT's flatMap adalah benar-benar tumpukan-aman (melalui Deferkendala pada jenis yang mendasari). Saya lebih memikirkan sesuatu seperti List, di mana berulang flatMaptidak aman-stack (itu memang memiliki halal tailRecM, meskipun).
Travis Brown