I was trying to do an exercise for a Cats Effect course in which I was required, among other things, to read a document line by line and wait a certain amount of time between lines.
The teacher's solution was the following:
def readLines(sc: Scanner): IO[Unit] =
if sc.hasNextLine then IO(sc.nextLine).debug *> IO.sleep(1.second) *> readLines(sc)
else IO.unit
However, I attempted to create a tail-recursive function from the same function above, and it looked something like this:
def readLines(sc: Scanner): IO[Unit] =
@tailrec
def tailReader(accum: IO[Unit]): IO[Unit] =
if sc.hasNextLine then tailReader(accum *> IO(sc.nextLine).debug *> IO.sleep(1.second))
else accum
tailReader(IO.unit)
end readLines
The problem is, the first implementation does read line by line and finishes. On the other hand, my implementation never finishes and never reads even the first line of the document, and is then followed by an OutOfMemoryError.
My question is, is there any way to implement the first function in a way that is tail-recursive and purely functional?
I know there is a way to make the first implementation stack-safe by using the >>
operator instead of the *>
one. However that's not what I'm looking for, instead I'm looking to be able to use the @tailrec
annotation.
The way I was able to partially fix the error was to create a value at the beginning of the function that saves the current line from the scanner, like this:
def readLines(sc: Scanner): IO[Unit] =
@tailrec
def tailReader(accum: IO[Unit]): IO[Unit] =
val currentLine = sc.nextLine
if sc.hasNextLine then tailReader(accum *> IO(currentLine).debug *> IO.sleep(1.second))
else accum >> IO(currentLine).debug.void
tailReader(IO.unit)
end readLines
Although this solved the problem, I am pretty sure that the solution is not purely functional.
I would be very grateful if you could give me a hand with this problem, I am just starting to understand the world of effects, and your help would come in handy!
For everything else, have a really nice day.
Some clarifications:
>>
and *>
: def *>[B](that: IO[B]): IO[B] =
productR(that)
def productR[B](that: IO[B]): IO[B] =
flatMap(_ => that)
def >>[B](that: => IO[B]): IO[B] =
flatMap(_ => that)
flatMap
because of how Monad is designed in Cats.I'll swap >>
and *>
with flatMap
in the following examples and drop @tailrec
as stack safety is already guaranteed.
Now what's wrong here
def readLines(sc: Scanner): IO[Unit] = {
def tailReader(accum: IO[Unit]): IO[Unit] = {
if (sc.hasNextLine)
tailReader {
accum
.flatMap(_ => IO(sc.nextLine()))
.flatMap(_ => IO.sleep(1.second))
}
else
tailReader(accum)
}
tailReader(IO.unit)
}
We're checking sc.hasNextLine
continually while not evaluating sc.nextLine
.
IO(sc.nextLine)
is only a description at this point, nothing is evaluated.
Make the recursive call part of the flatMap
chain to solve this:
def tailReader(accum: IO[Unit]): IO[Unit] = {
if (sc.hasNextLine)
IO(sc.nextLine())
.flatMap(_ => IO.sleep(1.second))
.flatMap(_ => tailReader(accum)) //make the call here
else
tailReader(accum)
}
then there's another small issue with the recursive method.
tailReader(..)
returns an IO only after it checks for next line once.
Use IO.suspend
to completely separate definition from evaluation.
def tailReader(accum: IO[Unit]): IO[Unit] = IO.suspend {
if (sc.hasNextLine)
IO(sc.nextLine())
.flatMap(_ => IO.sleep(1.second))
.flatMap(_ => accum)
else
tailReader(accum)
}
Now tailReader(IO.unit)
returns the IO without calling hasNextLine
even once.