4

I have 3 distinct modules each with its own error type. Following is a very simplified version.

object ModuleA {
  case class ErrorA(msg: String)
  def getA: ErrorA \/ String = "1".right
}

object ModuleB {
  case class ErrorB(msg: String)
  def getB(s: String): ErrorB \/ Int = 1.right
}

object ModuleC {
  case class ErrorC(msg: String)
  def getC(s: String, i: Int): ErrorC \/ Long = 1L.right
}

As a client of these modules what's the best way to chain these calls.

First - deeply nested, complex return type, but has all the types required.

def call1: ModuleA.ErrorA \/ (ModuleB.ErrorB \/ (ModuleC.ErrorC \/ Long)) = {
  ModuleA.getA.map { s =>
    ModuleB.getB(s).map { i =>
      ModuleC.getC(s, i)
    }
  }
}

Second - Very readable, but the error types are lost (Inferred return type is Product \/ Long). Ideally would want something similar with the error types

def call2  =
  for {
    s  <- ModuleA.getA
    i  <- ModuleB.getB(s)
    l  <- ModuleC.getC(s, i)
  } yield l

Third - Define new error types to encapsulate the existing ones. This seems unfeasible for different combinations

Lastly, tried to use EitherT, but seemed to get complex

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
Mridul
  • 41
  • 1
  • 1
    Does '3 distinct modules' mean that your intent is to keep them encapsulated and unaware of each other? If so – where is the client supposed to be located? One of those three modules? The fourth one? – Sotomajor Feb 20 '20 at 08:05
  • Yes, those could be 3 independent libraries. We would need one or more of those libraries in a client application. Example one of the lib deals with HDFS, one with Hive and the third with Configurations. – Mridul Feb 20 '20 at 23:08

1 Answers1

4

Consider creating an algebraic data type out of the errors, for example

sealed abstract class Error(val message: String)
case class ErrorA(msg: String) extends Error(msg)
case class ErrorB(msg: String) extends Error(msg)
case class ErrorC(msg: String) extends Error(msg)

and then change the left side of returned \/ to Error

import scalaz.\/
import scalaz.syntax.either._

object ModuleA {
  def getA: Error \/ String = "1".right
}

object ModuleB {
  def getB(s: String): Error \/ Int = ErrorB("boom").left
}

object ModuleC {
  def getC(s: String, i: Int): Error \/ Long = 1L.right
}

for {
  s  <- ModuleA.getA
  i  <- ModuleB.getB(s)
  l  <- ModuleC.getC(s, i)
} yield l

which gives

res0: Error \/ Long = -\/(ErrorB(boom))

If you cannot create ADT, consider leftMap to change the error type to a common type like so

case class ErrorWrapper(m: String)

for {
  s  <- ModuleA.getA.leftMap { e: ModuleA.ErrorA => ErrorWrapper(e.msg) }
  i  <- ModuleB.getB(s).leftMap { e: ModuleB.ErrorB => ErrorWrapper(e.msg) }
  l  <- ModuleC.getC(s, i).leftMap { e: ModuleC.ErrorC => ErrorWrapper(e.msg) }
} yield l
// res0: ErrorWrapper \/ Long = -\/(ErrorWrapper(boom))

or maybe even, unusually, via structural typing

implicit class CommonErrorWrapper[A <: Product { def msg: String }](e: A) {
  def toErrorWrapper: ErrorWrapper = ErrorWrapper(e.msg)
}

for {
  s  <- ModuleA.getA.leftMap(_.toErrorWrapper)
  i  <- ModuleB.getB(s).leftMap(_.toErrorWrapper)
  l  <- ModuleC.getC(s, i).leftMap(_.toErrorWrapper)
} yield l
// res1: ErrorWrapper \/ Long = -\/(ErrorWrapper(boom))

leftMap is useful not only for changing the error type, but also we can enrich the error by adding locally available contextual information.


Note EitherT monad transformer may be used when the shape of type is F[A \/ B], for example, Future[Error \/ B], however in your case it is just A \/ B, hence EitherT is might not be the right tool. Related question EitherT with multiple return types

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • 1
    I've been looking at this question, and I like your solution (the most recent version with the implicit class). One thing to add, perhaps, is that one can also combine both things: ADT and the mapping, and map the module-level errors into the 'application-level errors' that are represented as a sealed trait hierarchy. – Sotomajor Feb 20 '20 at 10:00
  • I guess there's no clean way to express this. Maybe with Dotty we could express as sum of error types. I haven't played around with Dotty so am not very sure ``` (ErrorA | ErrorB | Error C) \/ Long ``` – Mridul Feb 20 '20 at 23:15