What are some FS2 Error Hanlding Practices?

70 Views Asked by At

I am new to FS2, Cats Effect, etc., but I have been using Scala since 2005 and Akka since 2010...

I tuned up the FS2 example code to play with error-handling ideas, but I wondered if there are better idiomatic ways to deal with this.

I tried the various Stream mechanisms, but, it was very troublesome, and the best I could do was collapse the whole stream into a single output value. I had a lot of trouble finding good documentation on error handling in FS2.

import cats.effect.{IO, IOApp}
import fs2.{Stream, text}
import fs2.io.file.{Files, Path}

import scala.util.{Failure, Success}

object Converter extends IOApp.Simple {

  private val converter: Stream[IO, Unit] = {
    def fahrenheitToCelsius(f: Double) =
      (f - 32.0) * (5.0/9.0)

    def convert(string: String) =
      try
        Success(fahrenheitToCelsius(string.toDouble).toString)
      catch
        case cause: Exception => Failure(cause)

    Files[IO].readUtf8Lines(Path("testdata/fahrenheit.txt"))
      .filter(s => s.trim.nonEmpty && !s.startsWith("//"))
      .map(line => convert(line) match 
        case Success(value) => value
        case Failure(cause) => s"Failed to convert $line: ${cause.getMessage}"
      )
      .intersperse("\n")
      .through(text.utf8.encode)
      .through(Files[IO].writeAll(Path("testdata/celsius.txt")))
  }

  def run: IO[Unit] =
    converter.compile.drain
}
2

There are 2 best solutions below

0
Eric Kolotyluk On

Thanks to Luis Miguel Mejia Suarez, I tweaked this to become

import cats.effect.{IO, IOApp}
import fs2.{Stream, text}
import fs2.io.file.{Files, Path}

object Converter extends IOApp.Simple {

  private val converter: Stream[IO, Unit] = {
    def fahrenheitToCelsius(f: Double) =
      (f - 32.0) * (5.0/9.0)

    Files[IO].readUtf8Lines(Path("testdata/fahrenheit.txt"))
      .filter(s => s.trim.nonEmpty && !s.startsWith("//"))
      .map(line => line.toDoubleOption match
        case Some(value) => fahrenheitToCelsius(value).toString
        case None => s"Failed to convert $line to a number."
      )
      .intersperse("\n")
      .through(text.utf8.encode)
      .through(Files[IO].writeAll(Path("testdata/celsius.txt")))
  }

  def run: IO[Unit] =
    converter.compile.drain
}

If toDoubleOption did not exist, the right thing to do would have been to implement it. It might be worth implementing toDoubleEither or toDoubleTry as those do not seem to exist.

I had tried playing with Stream methods like handleError, HandleErrorWith, evalTap, evalFilter, etc., but I could not achieve what I wanted. I still have a lot to learn.

0
Eric Kolotyluk On

After more experimentation, I find the following more satisfying...

import cats.effect.{IO, IOApp}
import fs2.{Stream, text}
import fs2.io.file.{Files, Path}

import scala.util.{Failure, Success, Try}

object Converter extends IOApp.Simple {

  private val converter: Stream[IO, Unit] = {
    def fahrenheitToCelsius(f: Double) =
      (f - 32.0) * (5.0/9.0)

    Files[IO].readUtf8Lines(Path("testdata/fahrenheit.txt"))
      .filter(s => s.trim.nonEmpty && !s.startsWith("//"))
      .map(line => Try {line.toDouble} match
        case Success(value) => fahrenheitToCelsius(value).toString
        case Failure(cause) => s"Failed to convert $line: $cause"
      )
      .intersperse("\n")
      .through(text.utf8.encode)
      .through(Files[IO].writeAll(Path("testdata/celsius.txt")))
  }

  def run: IO[Unit] =
    converter.compile.drain
}

Sometimes, I forget how powerful Scala is, even without Cats Effect and FS2...