Is there a `OneOf` class for grouping classes with

2019-08-15 23:48发布

问题:

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?

回答1:

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
}


回答2:

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


标签: scala