Developus
Developus

Reputation: 1462

Scala, ZIO - how to convert EitherT to ZIO?

I have a simple method signature with returning type as EitherT:

def run(someEither: Either[SomeException, Statement], id: String): EitherT[Future, EncodingException, ResultSet]

But higher level method has a signature:

def someMethod(...) : zio.IO[SomeException, SomeResult]

I call run method inside someMethod like this:

run(someEither, data.getOrElse("empty")).bimap(
      { e: SomeException => IO.fail(e) },
      _ => IO.succeed(entity)
    )

But I got compilation error about Type mismatch - required: IO, found: EitherT. How it is possible to convert EitherT into zio.IO?

Upvotes: 3

Views: 1249

Answers (2)

Cory Klein
Cory Klein

Reputation: 55690

It depends on what your data looks like and how you want to handle failed futures, but here's a few options for if you don't care to handle specific kinds of Throwables from the Future:

// EitherT[Future, E, A] to IO[E, A]
val io: IO[E, A] =
  ZIO.fromFuture(eitherT.value)
    .orDie
    .flatMap(ZIO.fromEither)

// EitherT[Future, Throwable, A] to Task[A]
val task: Task[A] =
  ZIO
    .fromFuture(_ => et.value)
    .flatMap(ZIO.fromEither)

// EitherT[Future, Throwable, A] to UIO[A]
val task: UIO[A] =
  ZIO
    .fromFuture(_ => et.value)
    .flatMap(ZIO.fromEither)
    .orDie

Longer explanation

First off, there isn't a built-in for converting from EitherT, so you'll need to use the underlying Future, from which you can very quickly get to a ZIO Task:

val eitherT: EitherT[Future, A, B] = ???
val underlying: Future[Either[A, B]] = eitherT.value

// Note that this is equivalent to IO[Throwable, Either[...]]
val zioTask: Task[Either[MyError, MyResult]] = ZIO.fromFuture(_ => underlying)

// Or all at once:
val zioTask = ZIO.fromFuture(_ => eitherT.value)

I had a similar need as the OP, but in my case the Left is not an exception but rather my own MyError instead:

val underlying: Future[Either[MyError, MyResult]]

If you're in a similar boat, then you need to make a decision about what to do with any exceptions that might be in a possible failed future. One possible option is to...

Ignore them and let the fiber die if they happen

If the exception in the future represents a defect in the code, then often there is no other choice than to let the fiber die as it cannot proceed further. A fiber killed with orDie will have a Throwable representing the cause, but you have the option – which I recommend – to use your own exception that details how and why the failure occurred via orDieWith.

val uio: UIO[Either[MyError, MyResult]] = task.orDie

// Or

case class ExplanatoryException(cause: Throwable)
  extends RuntimeException(
    "A failure was encountered when we used XYZ-lib/made service call to FooBar/etc",
    cause
  )
val uio: UIO[Either[MyError, MyResult]] = task.orDieWith(ExplanatoryException)

// Then finally

val fullyConverted: IO[MyError, MyResult] = uio.flatMap(ZIO.fromEither)

Now from here you have a clean IO to work with!

Extension methods!

If you're doing this a lot you can add an extension method to EitherT to do the conversion for you:

implicit class EitherTOps[A, B](et: EitherT[Future, A, B]) {
  def toIO: IO[A, B] = ZIO.fromFuture(_ => et.value)
    .orDie
    .flatMap(ZIO.fromEither)

  def toIOWith(f: Throwable => Throwable): IO[A, B] = ZIO.fromFuture(_ => et.value)
    .orDieWith(f)
    .flatMap(ZIO.fromEither)
}

And beyond!

If you're able to constrain A <: Throwable then it makes it pretty easy to write some convenient toTask extension methods for EitherT, but I'll leave that as an exercise to the reader.

If you know a priori what kind of Throwable you may get from the Future and you want to actually handle it gracefully, then see this question which covers all the ways to do so.

Upvotes: 3

Boris Azanov
Boris Azanov

Reputation: 4491

I think you could do something like:

import zio._
import cats.data.EitherT
import scala.concurrent.Future

type ResultSet = ???

val eitherT: EitherT[Future, Exception, ResultSet] = ???
val result: IO[Throwable, ResultSet] = ZIO.fromFuture(_ => eitherT.value).flatMap(ZIO.fromEither)

Upvotes: 2

Related Questions