Во-первых, в этом нет необходимости, потому что IO
специально разработан для поддержки монадической рекурсии, безопасной для стека. Из документов:
IO
попал в ловушку в своей flatMap
оценке. Это означает, что вы можете безопасно вызывать flatMap
в рекурсивной функции произвольной глубины, не опасаясь взорвать стек ...
Таким образом, ваша реализация будет отлично работать с точки зрения безопасности стека, даже если вместо семи игроков вам понадобится 70000 игроков (хотя в этот момент вам, возможно, придется побеспокоиться о куче).
Однако это на самом деле не отвечает на ваш вопрос, и, конечно, даже @tailrec
никогда не необходим, поскольку все, что он делает, - это проверка того, что компилятор делает то, что, по вашему мнению, он должен делать.
Хотя невозможно написать этот метод таким образом, чтобы его можно было аннотировать с помощью @tailrec
, вы можете получить аналогичную уверенность, используя tailRecM
Cats. Например, следующее эквивалентно вашей реализации:
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
безопасен для стека, и одна из причин заключается в том, что вы можете когда-нибудь решить сделать свою программу универсальной в контексте эффекта (например, с помощью шаблона finally без тегов). В этом случае вы не должны предполагать, что flatMap
ведет себя так, как вы этого хотите, поскольку законность для cats.Monad
не требует, чтобы flatMap
был безопасным для стека. В этом случае было бы лучше избегать явных рекурсивных вызовов через flatMap
и вместо этого выбирать tailRecM
или iterateUntilM
и т. Д., Поскольку они гарантированно безопасны для стека для любого законного монадического контекста.
person
Travis Brown
schedule
02.02.2019