Вот фрагмент кода из документации для fs2 . Функция go
рекурсивная. Вопрос в том, как узнать, безопасен ли он для стека, и как определить, является ли какая-либо функция безопасной для стека?
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)
Будет ли это также безопасно для стека, если мы вызовем go
другой метод?
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
Лев Денисов
источник
источник
go
для использования, например,Monad[F]
typeclass - естьtailRecM
метод, позволяющий явно выполнять батут, чтобы гарантировать, что функция будет безопасна для стека. Возможно, я ошибаюсь, но без этого вы полагаетесь наF
то, что сами по себе безопасны в стеке (например, если он реализует батут внутри), но вы никогда не знаете, кто определит вашеF
, поэтому вам не следует этого делать. Если у вас нет гарантии, чтоF
это стек безопасно, используйте класс типов, который обеспечивает,tailRecM
потому что это безопасно по стеку по закону.@tailrec
аннотацией для функций хвостовой записи. Для других случаев нет никаких официальных гарантий в Scala AFAIK. Даже если сама функция безопасна, другие функции, которые она вызывает, могут не быть: /.Ответы:
Мой предыдущий ответ здесь дает некоторую справочную информацию , которая может оказаться полезной. Основная идея заключается в том, что некоторые типы эффектов имеют
flatMap
реализации, которые поддерживают рекурсию, безопасную для стека, - вы можете вкладыватьflatMap
вызовы либо явно, либо через рекурсию настолько глубоко, насколько захотите, и вы не переполните стек.Для некоторых типов эффектов невозможно
flatMap
обеспечить безопасность стека из-за семантики эффекта. В других случаях может быть возможно написать безопасный для стекаflatMap
, но разработчики, возможно, решили не делать этого из-за производительности или других соображений.К сожалению, не существует стандартного (или даже обычного) способа узнать, является ли
flatMap
для данного типа стек-безопасным. Cats включает в себяtailRecM
операцию, которая должна обеспечить безопасную для стека монадическую рекурсию для любого законного монадического типа эффекта, и иногда рассмотрениеtailRecM
реализации, которая, как известно, является законной, может дать некоторые подсказки о том, является ли объектflatMap
безопасным для стека. В случаеPull
это выглядит как это :Это
tailRecM
просто рекурсии черезflatMap
, и мы знаем , чтоPull
«sMonad
экземпляр является законным , что является довольно хорошим доказательством того, чтоPull
» sflatMap
есть стек-сейф. Один осложняющий фактор здесь является то , что экземпляр дляPull
имеетApplicativeError
ограничение наF
том , чтоPull
«sflatMap
не делает, но в данном случае это не меняет ничего.Таким образом,
tk
реализация здесь безопасна для стека, потому чтоflatMap
on безопасна дляPull
стека, и мы знаем это по ееtailRecM
реализации. (Если бы мы копали немного глубже, мы могли бы выяснить, чтоflatMap
это безопасно для стека, потому чтоPull
это, по сути, обертка дляFreeC
, которая трамплина .)Вероятно, было бы не очень сложно переписать
tk
с точки зренияtailRecM
, хотя мы должны были бы добавить иное ненужноеApplicativeError
ограничение. Я предполагаю, что авторы документации решили не делать этого для ясности, и потому что они знали,Pull
чтоflatMap
все в порядке.Обновление: вот довольно механический
tailRecM
перевод:Обратите внимание, что нет явной рекурсии.
Ответ на ваш второй вопрос зависит от того, как выглядит другой метод, но в случае с вашим конкретным примером
>>
это приведет к большему количествуflatMap
слоев, так что все должно быть в порядке.Чтобы ответить на ваш вопрос в более общем плане, вся эта тема - запутанный беспорядок в Scala. Вам не нужно копаться в реализациях, как мы делали выше, просто чтобы узнать, поддерживает ли тип монадическую рекурсию, ориентированную на стек, или нет. Здесь лучше помогли бы соглашения о документации, но, к сожалению, мы не очень хорошо справляемся с этим. Вы всегда можете использовать,
tailRecM
чтобы быть «безопасным» (это то, что вы захотите делать, когдаF[_]
, в любом случае, оно является универсальным), но даже тогда вы верите, чтоMonad
реализация законна.Подводя итог: это плохая ситуация, и в чувствительных ситуациях вы должны обязательно написать свои собственные тесты, чтобы убедиться, что подобные реализации безопасны для стека.
источник
go
другой метод, что может сделать его небезопасным? Если мы сделаем несколько нерекурсивных вычислений, прежде чем мы вызовемPull.output(hd) >> go(tl, n - m)
это нормально?ContT
«sflatMap
является на самом деле стек безопасно (черезDefer
ограничение на базовый тип). Я больше думал о чем-то вродеList
, когда повторение черезflatMap
не является безопасным для стека (tailRecM
хотя оно имеет законный характер ).