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
}