在monad内部工作时如何编写尾递归函数

时间:2019-02-02 10:28:32

标签: scala io monads tail-recursion cats-effect

总的来说,当在“内部” monad中工作时,我很难弄清楚如何编写tailrecursive函数。这是一个简单的示例:

这是我编写的一个小示例应用程序,用于更好地了解Scala中的FP。首先,提示用户进入一个由7名玩家组成的团队。此函数以递归方式读取输入:

import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._

case class Player (name: String)
case class Team (players: List[Player])

/**
  * Reads a team of 7 players from the command line.
  * @return
  */
def readTeam: IO[Team] = {
  def go(team: Team): IO[Team] = { // here I'd like to add @tailrec
    if(team.players.size >= 7){
      IO(println("Enough players!!")) >>= (_ => IO(team))
    } else {
      for {
        player <- readPlayer
        team   <- go(Team(team.players :+ player))
      } yield team
    }
  }
  go(Team(Nil))
}

private def readPlayer: IO[Player] = ???

现在我想实现的目标(主要是出于教育目的)是能够在@tailrec前面写一个def go(team: Team)表示法。但是我看不到将递归调用作为我的最后一条语句的可能性,因为据我所知,最后一条语句总是必须将我的团队“提升”到IO monad中。

任何提示将不胜感激。

1 个答案:

答案 0 :(得分:19)

首先,这不是必需的,因为IO是专为支持堆栈安全的单子递归而设计的。来自the docs

  

IO在其flatMap评估中得到了体现。这意味着您可以在任意深度的递归函数中安全地调用flatMap,而不必担心会炸毁堆栈……

因此,就堆栈安全而言,即使您需要7万名玩家(而不是7名玩家),您的实现也可以正常工作(尽管那时您可能需要担心堆)。

但是,这并没有真正回答您的问题,当然,@tailrec也从来都不是必要的,因为它所做的只是验证编译器是否在执行您认为应该做的事情做。

虽然无法以可以用@tailrec进行注释的方式编写此方法,但是可以通过使用Cats的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)))
}

这表示“从一个空团队开始,反复添加玩家,直到我们拥有所需的人数”,但没有任何明确的递归调用。只要monad实例是合法的(根据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进行显式递归调用,而选择tailRecMiterateUntilM等,因为这样可以保证在任何合法的单子上下文中它们都是安全的。