I think your question is somewhat ill-posed, but it's ill-posed in an interesting way, so maybe I should try to explain what I mean by that.
Asking "how do I implement
var i = 1
while(true) { i +=1; doSomething() }
in Cats" is answered trivially: in exactly the same way as you would implement it in ordinary Scala without using any libraries. In this particular case, Cats will not enable you to implement anything that behaves drastically different at runtime. What it does enable you to do is to express more precisely what (side) effects each piece of your code has, and to encode it as type level information, which can be verified at compile time during static type checking.
So, the question should not be
"How do I do X in Cats?"
but rather
"How do I prove / make explicit that my code does (or does not) have certain side effects using Cats?".
The while loop in your example simply performs some side effects in doSomething()
, and it messes with mutable state of the variable i
whenever it wants to, without ever making it explicit in the types of the constituent subexpressions.
Now you might take something like effects.IO
, and at least wrap the body of your doSomething
in an IO
, thereby making it explicit that it performs input/output operations (here: printing to StdOut):
// Your side-effectful `doSomething` function in IO
def doSomething: IO[Unit] = IO { println("do something") }
Now you might ask how to write down the loop in such a way that it also becomes obvious that it performs such IO-operations. You could do something like this:
// 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
Now, if you want to throw in the mutable variable i
into the mix, you could treat writing/reading of mutable memory as yet another IO operation:
// 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 ()
}
}
Alternatively, you might decide that tracking all accesses / changes of the state of i
is important enough that you want to make it explicit, for example using the StateT
monad transformer that keeps track of the state of type 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)
}
It would function similarly with two variables x
and y
(just use (Int, Int)
instead of Int
as the state).
Admittedly, this code seems a bit more verbose, and especially the last example begins to look like enterprise-edition fizz-buzz, but the point is that if you consistently apply these techniques to your code base, you don't have to dig into the body of a function to get a fairly good idea of what it can (or cannot) do, based on its signature alone. This in turn is advantageous when you are trying to understand the code (you can understand what it does by skimming over signatures, without reading the code in the bodies), and it also forces you to write simpler code that is easier to test (at least that's the idea).