Прежде всего, в этом нет необходимости, потому что IO
специально разработан для поддержки монадической рекурсии в стеке.Начиная с документы :
IO
находятся в батуте при оценке flatMap
.Это означает, что вы можете безопасно вызывать flatMap
в рекурсивной функции произвольной глубины, не боясь перевернуть стек…
Таким образом, ваша реализация будет работать нормально с точки зрения безопасности стека, даже если вместо этогоиз семи игроков вам понадобилось 70000 игроков (хотя в этот момент вам, возможно, придется беспокоиться о куче).
Хотя это не совсем отвечает на ваш вопрос, и, конечно, даже @tailrec
никогда не будет обязательно , поскольку все, что он делает, это проверяет, что компилятор делает то, что, как вы думаете, он должен делать.
Хотя невозможно написать этот метод таким образом, чтобы его можно было пометить @tailrec
, вы можете получить аналогичную гарантию, используя Кошки tailRecM
.Например, следующее соответствует вашей реализации:
import cats.effect.IO
import cats.syntax.functor._
case class Player (name: String)
case class Team (players: List[Player])
// For the sake of example.
def readPlayer: IO[Player] = IO(Player("foo"))
/**
* Reads a team of 7 players from the command line.
* @return
*/
def readTeam: IO[Team] = cats.Monad[IO].tailRecM(Team(Nil)) {
case team if team.players.size >= 7 =>
IO(println("Enough players!!")).as(Right(team))
case team =>
readPlayer.map(player => Left(Team(team.players :+ player)))
}
Это говорит «начните с пустой команды и многократно добавляйте игроков, пока у нас не будет необходимого номера», но без каких-либо явных рекурсивных вызовов.Пока экземпляр монады является законным (согласно определению Cats - есть некоторый вопрос о том, принадлежит ли tailRecM
даже к Monad
), вам не нужно беспокоиться о безопасности стека.
Как сторонапримечание: fa.as(b)
эквивалентно fa >>= (_ => IO(b))
, но более идиоматично.
Также в качестве дополнительного примечания (но, возможно, более интересного) вы можете написать этот метод еще более кратко (и, на мой взгляд, более четко).) следующим образом:
import cats.effect.IO
import cats.syntax.monad._
case class Player (name: String)
case class Team (players: List[Player])
// For the sake of example.
def readPlayer: IO[Player] = IO(Player("foo"))
/**
* Reads a team of 7 players from the command line.
* @return
*/
def readTeam: IO[Team] = Team(Nil).iterateUntilM(team =>
readPlayer.map(player => Team(team.players :+ player))
)(_.players.size >= 7)
Снова нет явных рекурсивных вызовов, и это даже более декларативно, чем версия tailRecM
- это просто "выполнять это действие итеративно, пока не выполнится данное условие".
Один постскриптум: вас может удивить, почему вы когда-либо использовали tailRecM
, когда IO#flatMap
безопасен для стека, и одна из причин заключается в том, что вы когда-нибудь решите сделать вашу программу универсальной в контексте эффекта (например, черезнаконец-то без тегов).В этом случае вы не должны предполагать, что flatMap
ведет себя так, как вы этого хотите, поскольку законность для cats.Monad
не требует flatMap
для обеспечения безопасности стека.В этом случае было бы лучше избегать явных рекурсивных вызовов через flatMap
и выбирать вместо них tailRecM
или iterateUntilM
и т. Д., Поскольку они гарантированно безопасны для стека для любого законного монадического контекста.