2

Similar to this, instead of wanting to accept one of several unrelated classes I'd like to return one.

I have an orchestrating service that utilizes several underlying Repositories. Each repo can pass back an error. However, these Error classes do not share a common ancestor.

For instance:

case class DistributionError
case class UserError
case class ContentError

I wanted to create my orchestrating service method like so:

def doSomethingComplicated(): Either[TheErrors, Boolean] = {

//...
  case Failure(e) => Left(new DistributionError)
//...
}

Then I would invoke it like so:

doSomethingComplicated() match {
   case Left(error) => {
      error match {
        case _: DistributionError => ...
        case _: UserError => ...
        case _: ContentError => ...
      }
   }
   case Right(success) => ...
}

As per the linked SO answer, I tried:

class TheErrors[T]
object TheErrors {
  implicit object DistWitness extends TheErrors[DistributionError]
  implicit object UserWitness extends TheErrors[UserError]
  implicit object ContentWitness extends TheErrors[ContentError]
}

But it just won't work the same way it does for parameters. The compiler always complains with:

> Error:(176, 48) type mismatch;  
>    found   : UserError
>    required: T
>             case None => Left(UserError)

Is it even possible to use this method for return types?

ThaDon
  • 7,826
  • 9
  • 52
  • 84

2 Answers2

2

AnyRef solution

The quick & cheap solution would be to drop the whole TheErrors typeclass, and simply return Either[AnyRef, Boolean] from doSomethingComplicated.

Existential types solution

If you absolutely want to ensure that doSomethingComplicated only returns types of errors that have previously been explicitly white-listed in TheErrors companion object, you can do this:

import scala.language.existentials

case class DistributionError()
case class UserError()
case class ContentError()

class TheErrors[T]
object TheErrors {
  implicit object DistWitness extends TheErrors[DistributionError]
  implicit object UserWitness extends TheErrors[UserError]
  implicit object ContentWitness extends TheErrors[ContentError]
}

def allowedError[E](e: E)(implicit witness: TheErrors[E])
: (E, TheErrors[E]) = (e, witness)

type AllowedError = (E, TheErrors[E]) forSome { type E }

def doSomethingComplicated(): Either[AllowedError, Boolean] = {
  import TheErrors._
  /* sth complicated */ Left(allowedError(DistributionError()))
}

doSomethingComplicated() match {
   case Left((error, _)) => {
      error match {
        case _: DistributionError => 42
        case _: UserError => 58
        case _: ContentError => 100
      }
   }
   case Right(success) => 2345678
}

Essentially, all it does is checking for an existence of a TheErrors-witness when you call allowedError, and attaching the witness to the error. This makes sure that only the errors for which the witnesses can be found are returned from doSomethingComplicated. Note however, that it does not help you to check the exhaustiveness of the pattern matching. For this, you would have to take the usual path, and wrap all your errors into subclasses of one common sealed trait.

Sealed trait solution

import scala.language.implicitConversions

case class DistributionError()
case class UserError()
case class ContentError()

sealed trait TheErrors
case class Distr(e: DistributionError) extends TheErrors
case class User(e: UserError) extends TheErrors
case class Content(e: ContentError) extends TheErrors

object TheErrors {
  implicit def apply(d: DistributionError): TheErrors = Distr(d)
  implicit def apply(d: UserError): TheErrors = User(d)
  implicit def apply(d: ContentError): TheErrors = Content(d)
}

def doSomethingComplicated(): Either[TheErrors, Boolean] = {
  /* sth complicated */ Left(DistributionError())
}

doSomethingComplicated() match {
   case Left(error) => {
      error match {
        case Distr(e) => 42
        case User(e) => 58
        case Content(e) => 100
      }
   }
   case Right(success) => 2345678
}

Implicit conversion + plain old subclass polymorphism

With implicit conversions and good old subclass polymorphism, you can get rid of any specific TheErrors subclasses in both doSomethingComplicated and in the caller code:

import scala.language.implicitConversions

case class DistributionError()
case class UserError()
case class ContentError()

sealed trait TheErrors {
  def error: AnyRef
}

object TheErrors {
  private case class TheError(val error: AnyRef) extends TheErrors
  implicit def apply(d: DistributionError): TheErrors = TheError(d)
  implicit def apply(d: UserError): TheErrors = TheError(d)
  implicit def apply(d: ContentError): TheErrors = TheError(d)
}

def doSomethingComplicated(): Either[TheErrors, Boolean] = {
  /* sth complicated */ Left(DistributionError())
}

doSomethingComplicated() match {
   case Left(e) => {
      e.error match {
        case _: DistributionError => 42
        case _: UserError => 58
        case _: ContentError => 100
      }
   }
   case Right(success) => 2345678
}
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • Sealed Traits looks nice and clean in it's implementation, but I guess there's the layer of abstraction above the actual error types (i.e. `Distr(e)`) that the caller has to deal with. In the Existential Type example, the caller still has to deal with some abstraction however they get to deal with the base error types, which I think I like more (although certainly involve a lot of boilerplate!). However, for sealed traits, I should be able to add an implicit conversion for, say, `Distr -> DistributionError` and that would allow the caller to not have to deal with that container type right? – ThaDon Jun 30 '18 at 13:42
  • @ThaDon Added implicit conversions to reduce some of the boilerplate inside `doSomethingComplicated`. – Andrey Tyukin Jun 30 '18 at 13:49
  • I guess there's no way I can deal, from the caller's perspective, with the base Error types? Instead of switching on `Distr`, `User`, `Content` instead switch on `DistributionError`, `UserError`, `ContentError` – ThaDon Jun 30 '18 at 15:17
  • @ThaDon Added yet another update; Now you don't see anything of the case classes. Just implicit conversions on the definition side, and plain old subtype polymorphism on the caller side. – Andrey Tyukin Jun 30 '18 at 16:54
  • 1
    Thank you. You went above and beyond the call of duty on this one. – ThaDon Jun 30 '18 at 18:05
  • Just a side note, I wasted a bunch of time because I didn't at first realize that the ``private case class` within the `TheErrors` object wasn't pluralized! As well, the value each `apply` method returns is not pluralized as well. The thing is... it'll compile just fine, it's not until runtime that things just blow up (without any error in my case). I hope I've saved someone else some time and heartache! – ThaDon Jul 05 '18 at 02:38
1

Few options:

  • Use Shapeless's Coproduct type
  • Use Any
  • Nested Either i.e. Either[Either[Either[A, B], C], D]
  • Define your own sealed trait with subclasses that just wrap their corresponding types i.e. class UserErrorWrapper(val u: UserError) extends MyErrorTrait
  • Wait for Dotty and use a union type
Joe K
  • 18,204
  • 2
  • 36
  • 58
  • Union type looks nice. Do you know if they’ll be valid as a return type? The examples all showed then being used in a parameter list. – ThaDon Jun 30 '18 at 01:50
  • 1
    Yep, they'll be a type just like any other – Joe K Jun 30 '18 at 07:30