I actually believe they are essential to start with, assuming that the motivation is to help reduce boilerplate, and I don’t find much boilerplate in simple (1 level) ADT hierarchies.
I think it’s crucial to explore how this new syntax is going to help remove boilerplate or create a simpler and more readable syntax for ADTs. For that, I believe we need actual examples, comparing between the old and the potentially new syntax.
I have one such example of my own, in which my ADTs also have def
s in them (I’d imagine this is a fairly common use-case?). My ADTs represent a container of a single value, somewhat emulating a REST response. It’s similar to Option
, but rather than have one “none” case, there are many variations of no-value cases.
You could find the following code examples – along with a variation of the Scala 2 implementation that includes scaladocs – in this gist.
- Implementation in Scala 2:
sealed trait Response[+A] {
def returnValue: Option[A]
def map[B](f: A => B): Response[B]
def flatMap[B](f: A => Response[B]): Response[B]
}
case class Success[+A](value: A) extends Response[A] {
override val returnValue: Option[A] = value match {
case _: Unit => None
case _ => Option(value)
}
override def map[B](f: (A) => B): Response[B] =
try {
Success(f(value))
} catch {
case NonFatal(e) => GenericFailure(s"Failed to map service response with value: $value", Some(e))
}
override def flatMap[B](f: A => Response[B]): Response[B] = {
f(value)
}
}
object Success {
def empty: Success[Unit] = Success(())
}
trait NoValueResponse {
self: Response[Nothing] =>
override val returnValue: Option[Nothing] = None
override def map[B](f: (Nothing) => B): Response[B] = this
override def flatMap[B](f: (Nothing) => Response[B]): Response[B] = this
}
object NoChange extends Response[Nothing] with NoValueResponse
object Accepted extends Response[Nothing] with NoValueResponse
sealed trait Failure extends Response[Nothing] with NoValueResponse {
def message: String
}
case class NoSuchElement(message: String) extends Failure
case class IllegalArgument(message: String, cause: Option[Throwable] = None) extends Failure
case class GenericFailure(message: String, cause: Option[Throwable] = None) extends Failure
- Implementation in hypothetical multi-level ADT syntax:
sealed trait Response[+A] {
def returnValue: Option[A]
def map[B](f: A => B): Response[B]
def flatMap[B](f: A => Response[B]): Response[B]
case Success(value: A) {
override val returnValue: Option[A] = value match {
case _: Unit => None
case _ => Option(value)
}
override def map[B](f: (A) => B): Response[B] =
try {
Success(f(value))
} catch {
case NonFatal(e) => GenericFailure(s"Failed to map service response with value: $value", Some(e))
}
override def flatMap[B](f: A => Response[B]): Response[B] = {
f(value)
}
}
case sealed trait NoValueResponse {
override val returnValue: Option[Nothing] = None
override def map[B](f: (Nothing) => B): Response[B] = this
override def flatMap[B](f: (Nothing) => Response[B]): Response[B] = this
case NoChange
case Accepted
case sealed trait Failure {
def message: String
case NoSuchElement(message: String)
case IllegalArgument(message: String, cause: Option[Throwable] = None)
case GenericFailure(message: String, cause: Option[Throwable] = None)
}
}
}
object Success {
def empty: Success[Unit] = Success(())
}
- Implementation in hypothetical multi-level ADT syntax with the
def
s as extension methods:
sealed trait Response[+A] {
case Success(value: A)
case sealed trait NoValueResponse {
case NoChange
case Accepted
case sealed trait Failure {
case NoSuchElement(message: String)
case IllegalArgument(message: String, cause: Option[Throwable] = None)
case GenericFailure(message: String, cause: Option[Throwable] = None)
}
}
}
object Response {
implicit class Ops[A](response: Response[A]) {
def returnValue: Option[A] = response match {
case Success(value) => value match {
case _: Unit => None
case _ => Option(value)
}
case _: NoValueResponse => None
}
def map[B](f: A => B): Response[B] = response match {
case Success(value) =>
try {
Success(f(value))
} catch {
case NonFatal(e) => GenericFailure(s"Failed to map service response with value: $value", Some(e))
}
case noValue: NoValueResponse => noValue
}
def flatMap[B](f: A => Response[B]): Response[B] = response match {
case Success(value) =>f(value)
case noValue: NoValueResponse => noValue
}
}
}
object Success {
def empty: Success[Unit] = Success(())
}
object Failure
implicit class Ops(failure: Failure) {
def message: String = failure match {
case NoSuchElement(message) => message
case IllegalArgument(message, _) => message
case GenericFailure(message, _) => message
}
}
}
I believe we should examine more cases like this and consider whether they are worth the trouble.