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?
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
}