在猫中实现 while(true)

Implement while(true) in cats

如何在猫中实现以下循环?

首先(正常的 while(true) 循环):

while(true) { doSomething() }

第二个(带增量的 while(true) 循环):

var i = 1
while(true) { i +=1; doSomething() }

第三个(while(true)里面有几个自变量):

var x = 1
var y = 2
while(true) {
  x = someCalculation()
  y = otherCalculation()
  doSomething()
}

在您的情况下,while(true) 循环的功能替代方案只是递归函数:

@tailrec
def loop(x: Int, y: Int) { //every call of loop will receive updated state
    doSomething()
    loop(someCalculation(), otherCalculation()) //next iteration with updated state
}

loop(1,2); //start "loop" with initial state

这里很重要的是@tailrec注解。它检查对 loop 的递归调用是否在尾部位置,因此可以应用 tail-call optimalization。如果不是这种情况,您的 "loop" 会导致 WhosebugException

一个有趣的事实是,优化后的函数在字节码中看起来与 while 循环非常相似。

这种方法与猫并没有​​直接关系,而是与函数式编程有关。递归在 FP 中非常常用。

我认为你的问题有点 ill-posed,但它 ill-posed 的方式很有趣,所以也许我应该试着解释一下我的意思。

询问“我如何实施

var i = 1
while(true) { i +=1; doSomething() }

在 Cats 中的回答很简单:与在不使用任何库的情况下在普通 Scala 中实现它的方式完全相同。在这种特殊情况下,Cats 不会启用您实现任何在运行时行为截然不同的东西。它使您能够做的是更准确地表达每段代码的(副作用),并将其编码为类型级别信息,可以在编译时验证在静态类型检查期间。

所以,问题不应该是

"How do I do X in Cats?"

而是

"How do I prove / make explicit that my code does (or does not) have certain side effects using Cats?".

您的示例中的 while 循环只是在 doSomething() 中执行一些副作用,并且它会在需要时随时扰乱变量 i 的可变状态,而不会在类型中明确显示组成子表达式的数量。

现在你可以采用 effects.IO 之类的东西,并且至少将 doSomething 的主体包裹在 IO 中,从而明确表示它执行 input/output操作(此处:打印到 StdOut):

// Your side-effectful `doSomething` function in IO
def doSomething: IO[Unit] = IO { println("do something") }

现在您可能会问如何以这样一种方式写下循环,使得它执行这样的操作也变得显而易见 IO-operations。你可以这样做:

// Literally `while(true) { ... }`
def whileTrue_1: IO[Unit] =
  Monad[IO].whileM_(IO(true)) { doSomething }

// Shortcut using `foreverM` syntax
import cats.syntax.flatMap._
def whileTrue_2: IO[Nothing] = doSomething.foreverM

// Use `>>` from `syntax.flatMap._`
def whileTrue_3: IO[Unit] = doSomething >> whileTrue_3

现在,如果您想将可变变量 i 放入混合中,您可以将可变内存的 writing/reading 视为另一个 IO 操作:

// Treat access to mutable variable `i` as
// yet another IO side effect, do something
// with value of `i` again and again.
def whileInc_1: IO[Unit] = {
  var i = 1
  def doSomethingWithI: IO[Unit] = IO {
    println(s"doing sth. with $i")
  }

  Monad[IO].whileM_(IO(true)) {
    for {
      _ <- IO { i += 1 }
      _ <- doSomethingWithI
    } yield ()
  }
}

或者,您可能会决定跟踪 i 状态的所有访问/更改非常重要,以至于您希望将其明确化,例如使用 StateT monad transformer 来跟踪Int:

类型的状态
// Explicitly track variable `i` in a state monad
import cats.data.StateT
import cats.data.StateT._
def whileInc_2: IO[Unit] = {

  // Let's make `doSthWithI` not too boring,
  // so that it at least accesses the state
  // with variable `i`
  def doSthWithI: StateT[IO, Int, Unit] =
    for {
      i <- get[IO, Int]
      _ <- liftF(IO { println(i) })
    } yield ()

  // Define the loop
  val loop = Monad[StateT[IO, Int, ?]].whileM_(
    StateT.pure(true)
  ) {
    for {
      i <- get[IO, Int]
      _ <- set[IO, Int](i + 1)
      _ <- doSthWithI
    } yield ()
  }

  // The `_._2` is there only to make the
  // types match up, it's never actually used,
  // because the loop runs forever.
  loop.run(1).map(_._2)
}

它与两个变量 xy 的功能类似(只需使用 (Int, Int) 而不是 Int 作为状态)。

诚然,这段代码看起来有点冗长,尤其是最后一个示例开始看起来像 enterprise-edition fizz-buzz,但关键是如果您始终如一地将这些技术应用于您的代码库,您不必深入研究函数体即可仅根据其签名就可以很好地了解它可以(或不能)做什么。当你试图理解代码时,这反过来是有利的(你可以通过浏览签名来理解它的作用,而无需阅读正文中的代码),并且它还迫使你编写更容易测试的更简单的代码(在至少这是个主意。