在猫中实施while(true)

时间:2019-05-09 09:22:49

标签: scala scala-cats

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

第一个(正常的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()
}

2 个答案:

答案 0 :(得分:5)

我认为您的问题有些不恰当,但它以一种有趣的方式引起了不适,所以也许我应该尝试解释一下我的意思。

询问”我该如何实现

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

在Cats中” 的回答很简单:与在普通Scala中实现它的方式完全相同,而无需使用任何库。在这种特殊情况下,Cats无法使您实现任何行为它使您能够做的是更精确地表达每段代码所具有的(副作用),并将其编码为类型级别信息,这些信息可以在编译时在静态类型检查期间进行验证。

因此,问题不应是

  

“我如何在猫中做X?”

但是

  

“我如何证明 /明确指出我的代码使用Cats是否(或没有)某些副作用?”

您的示例中的while循环仅在doSomething()中执行一些副作用,并且在变量i想要改变的任何时候都陷入混乱状态,而从未在变量的类型中明确组成子表达式。

现在,您可能会使用类似effects.IO的东西,并且至少将doSomething的主体包裹在IO中,从而使其明确执行输入/输出操作(此处:打印到StdOut):

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

现在,您可能会问如何以这样一种方式写下循环,以使其显然也可以执行此类IO操作。您可以执行以下操作:

// 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放入混合中,则可以将对可变内存的写入/读取视为另一个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转换器来跟踪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作为状态)。

诚然,这段代码看起来有些冗长,尤其是最后一个示例开始看起来像是企业版的“嗡嗡声”,但要点是,如果您始终将这些技术应用于代码库,则无需深入研究函数的主体,以仅基于其签名就可以(或不能)做什么做一个相当好的主意。反过来,这在尝试理解代码时是有利的(您可以通过略读签名来了解其功能,而无需阅读主体中的代码),并且还迫使您编写更易于测试的简单代码(在至少是这个主意)。

答案 1 :(得分:2)

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。如果不是这种情况,您的“循环” 将导致StackOverflowException

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

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