How to combine ADTs in Scala?

Viewed 75

I have two layers in my app: domain and application. Each layer has its own "error" ADT. For instance:

package com.domain.person

sealed trait DomainError
case object NoPermission extends DomainError

final case class Person(hasPermission: Boolean): Either[DomainError, ???] {
  def doSomething() = {
    if (!hasPermission)
      Left(NoPermission)
    else
      ...
  }
}

and in my application layer (another package):

package com.application.person

sealed trait ApplicationError
case object PersonNotFound extends ApplicationError
case object UnexpectedFatalError extends ApplicationError

// and a function f :: Either ApplicationError Something

The issue is, since DomainError lives in another package, I can't just simply extend my ApplicationError trait:

sealed trait ApplicationError extends DomainError // compilation error

I could create yet another case object to wrap DomainError:

sealed trait ApplicationError
// n list of errors, and then:
final case class WrappedDomainError(d: DomainError) extends ApplicationError

but that solution is suboptimal at best.

And also, what if I want to be more specific in my doSomething() and, instead of returning a whole DomainError, a different subset?

doSomething :: Either DoSomethingErrors ???

I would have to account for all cases in each of my domain layer's functions.

Is there any way I can do a proper sum type in Scala?

Thanks

1 Answers

Wrapping your domain error in application error is not a bad idea, TBH. It's what I would've done in your situation. A few more options to consider:

  1. make your DomainError and ApplicationError extends a common supertype Error, CommonError, Failure, etc. My personal preference is to extend Throwable - this way your error ASTs can become isomorphic to exceptions which can come in handy for Java interop reasons.

  2. error channel also being composed of unions. Your final type will look somewhat like Either[ApplicationError Either DomainError, A]. It's a bit mouthful but you can make it look less ugly by introducing aliases.

type Result[+A] = Either[ApplicationError Either DomainError, A]

def doSomething: Result[???]
  1. replace either with your own AST or use scalaz or other library's alternatives to Either3
sealed trait Result[+A]
case class Success[A](a: A) extends Result[A]
case class ApplicationErr(err: ApplicationError) extends Result[Nothing]
case class DomainErr[A](err: DomainErr) extends Result[Nothing]

def doSomething: Result[???]
  1. Interpret DomainErrors into ApplicationErrors
val maybeDomainErrorVal: Either[DomainError, ???] = ???
val maybeApplicationErrorVal: Either[ApplicationError, ???] = 
  maybeDomainErrorVal.leftMap {
    case NoPermission => UnexpectedFatalError
  }
Related