Aquí hay un fragmento de código de la documentación para fs2 . La función go
es recursiva. La pregunta es ¿cómo sabemos si es seguro para la pila y cómo razonar si alguna función es segura para la pila?
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)
¿También sería seguro para la pila si llamamos go
desde otro método?
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
fuente
fuente
go
para usar, por ejemplo,Monad[F]
typeclass: hay untailRecM
método que le permite realizar el trampolín explícitamente para garantizar que la función sea segura para la pila. Puede que me equivoque, pero sin esoF
dependes de la seguridad de la pila por sí solo (por ejemplo, si implementa el trampolín internamente), pero nunca sabes quién definirá tuF
, así que no deberías hacer esto. Si no tiene garantía de queF
sea apilable, use una clase de tipo que proporcionetailRecM
porque es apta para la ley.@tailrec
anotaciones para las funciones de grabación de cola. Para otros casos no hay garantías formales en Scala AFAIK. Incluso si la función en sí es segura, las otras funciones que está llamando podrían no ser: /.Respuestas:
Mi respuesta anterior aquí proporciona información de fondo que podría ser útil. La idea básica es que algunos tipos de efectos tienen
flatMap
implementaciones que admiten directamente la recursividad segura para la pila; puede anidarflatMap
llamadas explícitamente o mediante recursión tan profundamente como desee y no desbordará la pila.Para algunos tipos de efectos no es posible
flatMap
apilar con seguridad debido a la semántica del efecto. En otros casos, es posible escribir una pila seguraflatMap
, pero los implementadores pueden haber decidido no hacerlo debido al rendimiento u otras consideraciones.Desafortunadamente, no hay una forma estándar (o incluso convencional) de saber si
flatMap
un tipo determinado es apilable. Los gatos incluyen unatailRecM
operación que debería proporcionar una recidiva monádica segura para cualquier tipo de efecto monádico legal, y a veces mirar unatailRecM
implementación que se sabe que es legal puede proporcionar algunas pistas sobre si unaflatMap
pila es segura para la pila. En el caso dePull
que se parece a esto :Esto
tailRecM
es sólo a través recursivaflatMap
, y sabemos quePull
'sMonad
ejemplo, es legal , lo cual es bastante buena evidencia de quePull
' sflatMap
es pila de fallos. El único factor de complicación aquí es que la instancia dePull
tiene unaApplicativeError
restricción enF
quePull
'sflatMap
no lo hace, pero en este caso eso no cambia nada.Entonces, la
tk
implementación aquí es seguraflatMap
para laPull
pila porque on es segura para la pila, y lo sabemos al observar sutailRecM
implementación. (Si profundizáramos un poco más, podríamos descubrir queflatMap
es apta para apilar porquePull
es esencialmente un envoltorio paraFreeC
, que está trampolinado ).Probablemente no sería terriblemente difícil reescribir
tk
en términos detailRecM
, aunque tendríamos que agregar laApplicativeError
restricción innecesaria . Supongo que los autores de la documentación decidió no hacerlo por razones de claridad, y porque sabíanPull
'sflatMap
está muy bien.Actualización: aquí hay una
tailRecM
traducción bastante mecánica :Tenga en cuenta que no hay una recursión explícita.
La respuesta a su segunda pregunta depende de cómo se ve el otro método, pero en el caso de su ejemplo específico,
>>
solo dará como resultado másflatMap
capas, por lo que debería estar bien.Para abordar su pregunta de manera más general, todo este tema es un desastre confuso en Scala. No debería tener que profundizar en las implementaciones como lo hicimos anteriormente solo para saber si un tipo admite la recursión monádica segura para la pila o no. Las mejores convenciones sobre documentación serían una ayuda aquí, pero desafortunadamente no estamos haciendo un buen trabajo al respecto. Siempre se puede usar
tailRecM
para ser "seguro" (que es lo que querrá hacer cuandoF[_]
sea genérico, de todos modos), pero aun así confía en que laMonad
implementación es legal.En resumen: es una mala situación en general, y en situaciones delicadas definitivamente debes escribir tus propias pruebas para verificar que implementaciones como esta sean aptas para la pila.
fuente
go
desde otro método, ¿qué puede hacer que se apile inseguro? Si hacemos algunos cálculos no recursivos antes de llamar, ¿Pull.output(hd) >> go(tl, n - m)
está bien?ContT
'sflatMap
es realmente pila de fallos (a través de unaDefer
restricción sobre el tipo subyacente). Estaba pensando más en algo asíList
, donde recurrirflatMap
no es seguro para la pila (aunque sí es legaltailRecM
).