Aqui está um pedaço de código da documentação para fs2 . A função go
é recursiva. A questão é: como sabemos se a pilha é segura e como raciocinar se alguma função é segura?
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)
Também seria seguro para a pilha se chamarmos go
de outro 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
fonte
fonte
go
para usar, por exemplo,Monad[F]
typeclass - existe umtailRecM
método que permite executar trampolim explicitamente para garantir que a função seja segura para a pilha. Eu posso estar errado, mas sem ele você está confiando emF
ser seguro por si próprio (por exemplo, se implementar trampolim internamente), mas você nunca sabe quem definirá o seuF
, por isso não deve fazer isso. Se você não tiver garantia de que aF
pilha é segura, use uma classe de tipo que for fornecidatailRecM
porque é segura pela pilha por lei.@tailrec
anotação para funções de gravação de cauda. Para outros casos, não há garantias formais no Scala AFAIK. Mesmo que a função em si seja segura, as outras funções que está chamando podem não ser: /.Respostas:
Minha resposta anterior aqui fornece algumas informações básicas que podem ser úteis. A idéia básica é que alguns tipos de efeito tenham
flatMap
implementações que suportam diretamente a recursão segura para a pilha - você pode aninharflatMap
chamadas explicitamente ou por recursão tão profundamente quanto desejar e não excederá a pilha.Para alguns tipos de efeito, não é possível
flatMap
ter segurança na pilha, devido à semântica do efeito. Em outros casos, pode ser possível escrever um cofre de pilhaflatMap
, mas os implementadores podem ter decidido não fazê-lo por causa do desempenho ou de outras considerações.Infelizmente, não existe uma maneira padrão (ou mesmo convencional) de saber se o
flatMap
tipo de dado é seguro para a pilha. Cats inclui umatailRecM
operação que deve fornecer recursão monádica segura para qualquer tipo de efeito monádico legal e, às vezes, observar umatailRecM
implementação que seja legal pode fornecer algumas dicas sobre se aflatMap
pilha é segura. No caso dePull
ele se parece com isso :Esta
tailRecM
é apenas recursão atravésflatMap
, e nós sabemos quePull
'sMonad
exemplo é lícito , o que é bastante boa evidência de quePull
' sflatMap
é pilha-safe. O único complicador aqui é que a instância para quePull
tem umaApplicativeError
restrição sobreF
quePull
'sflatMap
não, mas neste caso isso não muda nada.Portanto, a
tk
implementação aqui é segura para a pilha, porqueflatMap
onPull
é segura para a pilha, e sabemos disso ao analisar suatailRecM
implementação. (Se aprofundarmos um pouco mais, poderemos descobrir queflatMap
é seguro para a pilha, porquePull
é essencialmente um invólucroFreeC
, que é trampolim .)Provavelmente não seria terrivelmente difícil reescrever
tk
em termos detailRecM
, embora tenhamos que adicionar aApplicativeError
restrição desnecessária . Eu estou supondo que os autores da documentação escolheu não fazer isso para maior clareza, e porque sabiamPull
éflatMap
está bem.Atualização: aqui está uma
tailRecM
tradução bastante mecânica :Observe que não há recursão explícita.
A resposta para sua segunda pergunta depende da aparência do outro método, mas, no caso de seu exemplo específico,
>>
resultará apenas em maisflatMap
camadas, portanto tudo ficará bem.Para responder à sua pergunta de maneira mais geral, todo esse tópico é uma bagunça confusa no Scala. Você não precisa se aprofundar nas implementações, como fizemos acima, apenas para saber se um tipo suporta recursão monádica segura para pilha ou não. Melhores convenções sobre documentação seriam uma ajuda aqui, mas infelizmente não estamos fazendo um bom trabalho nisso. Você sempre pode
tailRecM
ser "seguro" (que é o que você deseja fazer quandoF[_]
for genérico, de qualquer maneira), mas mesmo assim você confia que aMonad
implementação é legal.Resumindo: é uma situação ruim e, em situações sensíveis, você definitivamente deve escrever seus próprios testes para verificar se implementações como essa são seguras para a pilha.
fonte
go
de outro método, o que pode torná-lo inseguro? Se fizermos alguns cálculos não recursivos antes de chamarmos,Pull.output(hd) >> go(tl, n - m)
está tudo bem?ContT
'sflatMap
é realmente pilha-safe (através de umaDefer
restrição sobre o tipo subjacente). Eu estava pensando mais em algo comoList
, onde a recorrênciaflatMap
não é segura para a pilha (mas tem uma legalidadetailRecM
).