Berikut adalah sepotong kode dari dokumentasi untuk fs2 . Fungsi go
ini 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 go
dari 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
}
scala
functional-programming
tail-recursion
scala-cats
fs2
Lev Denisov
sumber
sumber
go
untuk menggunakan mis.Monad[F]
Typeclass - adatailRecM
metode yang memungkinkan Anda untuk melakukan trampolin secara eksplisit untuk menjamin bahwa fungsi tersebut akan menjadi stack safe. Saya mungkin salah tetapi tanpa itu Anda mengandalkanF
tumpukan aman sendiri (misalnya jika itu menerapkan trampolin secara internal), tetapi Anda tidak pernah tahu siapa yang akan menentukan AndaF
, jadi Anda tidak harus melakukan ini. Jika Anda tidak memiliki jaminan bahwa ituF
adalah stack safe, gunakan kelas tipe yang menyediakantailRecM
karena itu adalah stack-safe oleh hukum.@tailrec
anotasi 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: /.Jawaban:
Jawaban saya sebelumnya di sini memberikan beberapa informasi latar belakang yang mungkin berguna. Ide dasarnya adalah bahwa beberapa tipe efek memiliki
flatMap
implementasi yang mendukung rekursi aman-stack secara langsung — Anda dapat membuat sarangflatMap
panggilan secara eksplisit atau melalui rekursi sedalam yang Anda inginkan dan Anda tidak akan meluap tumpukan.Untuk beberapa tipe efek, tidak mungkin
flatMap
menjadi stack-safe, karena efek semantiknya. Dalam kasus lain dimungkinkan untuk menulis stack-safeflatMap
, tetapi pelaksana mungkin memutuskan untuk tidak melakukannya karena kinerja atau pertimbangan lain.Sayangnya tidak ada standar (atau bahkan konvensional) cara untuk mengetahui apakah
flatMap
tipe yang diberikan adalah stack-safe. Kucing memang menyertakantailRecM
operasi yang harus menyediakan rekursi monadik yang aman untuk semua jenis efek monadat yang sah, dan terkadang melihattailRecM
implementasi yang dikenal sah dapat memberikan beberapa petunjuk tentang apakahflatMap
aman-stack. JikaPull
terlihat seperti ini :Ini
tailRecM
hanya recursing melaluiflatMap
, dan kita tahu bahwaPull
'sMonad
contoh adalah halal , yang merupakan bukti yang cukup bagus bahwaPull
' sflatMap
yaitu tumpukan-aman. Satu faktor rumit di sini adalah bahwa contoh untukPull
memilikiApplicativeError
kendala padaF
yangPull
'sflatMap
tidak, tetapi dalam kasus ini yang tidak berubah apa-apa.Jadi
tk
implementasinya di sini adalah stack-safe karenaflatMap
onPull
-stack-safe, dan kita tahu dari melihattailRecM
implementasinya. (Jika kita menggali sedikit lebih dalam, kita bisa mengetahui bahwaflatMap
itu aman-tumpukan karenaPull
pada dasarnya adalah pembungkusFreeC
, yang diinjak - injak .)Mungkin tidak akan terlalu sulit untuk menulis ulang
tk
dalam haltailRecM
, meskipun kita harus menambahkanApplicativeError
kendala yang tidak perlu . Saya menduga penulis dokumentasi memilih untuk tidak melakukan itu untuk kejelasan, dan karena mereka tahuPull
'sflatMap
baik-baik saja.Pembaruan: inilah terjemahan yang cukup mekanis
tailRecM
: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 banyakflatMap
lapisan, 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
tailRecM
untuk menjadi "aman" (yang adalah apa yang ingin Anda lakukan ketikaF[_]
generik, toh), tetapi bahkan kemudian Anda percaya bahwaMonad
penerapannya 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.
sumber
go
dari 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?ContT
'sflatMap
adalah benar-benar tumpukan-aman (melaluiDefer
kendala pada jenis yang mendasari). Saya lebih memikirkan sesuatu sepertiList
, di mana berulangflatMap
tidak aman-stack (itu memang memiliki halaltailRecM
, meskipun).