As has been discussed many times on SO, a Scala match will warn you if you don\'t exhaustively list all of the types deriving from a sealed class.
What I want is a
Here's a working example using macros on 2.10.0-M6:
(update: to make this example work in 2.10.0-M7, you need to replace c.TypeTag with c.AbsTypeTag; to make this example work in 2.10.0-RC1, c.AbsTypeTag needs to be replaced with c.WeakTypeTag)
import scala.reflect.makro.Context
object SealednessMacros {
def exhaustive[P](ps: Seq[P]): Seq[P] = macro exhaustive_impl[P]
def exhaustive_impl[P: c.TypeTag](c: Context)(ps: c.Expr[Seq[P]]) = {
import c.universe._
val symbol = typeOf[P].typeSymbol
val seen = ps.tree match {
case Apply(_, xs) => xs.map {
case Select(_, name) => symbol.owner.typeSignature.member(name)
case _ => throw new Exception("Can't check this expression!")
}
case _ => throw new Exception("Can't check this expression!")
}
val internal = symbol.asInstanceOf[scala.reflect.internal.Symbols#Symbol]
if (!internal.isSealed) throw new Exception("This isn't a sealed type.")
val descendants = internal.sealedDescendants.map(_.asInstanceOf[Symbol])
val objs = (descendants - symbol).map(
s => s.owner.typeSignature.member(s.name.toTermName)
)
if (seen.toSet == objs) ps else throw new Exception("Not exhaustive!")
}
}
This obviously isn't very robust (for example, it assumes that you only have objects in the hierarchy, and it'll fail on A :: B :: C :: Nil
), and it still requires some unpleasant casting, but it works as a quick proof-of-concept.
First we compile this file with macros enabled:
scalac -language:experimental.macros SealednessMacros.scala
Now if we try to compile a file like this:
object MyADT {
sealed trait Parent
case object A extends Parent
case object B extends Parent
case object C extends Parent
}
object Test extends App {
import MyADT._
import SealednessMacros._
exhaustive[Parent](Seq(A, B, C))
exhaustive[Parent](Seq(C, A, B))
exhaustive[Parent](Seq(A, B))
}
We'll get a compile-time error on the Seq
with the missing C
:
Test.scala:14: error: exception during macro expansion:
java.lang.Exception: Not exhaustive!
at SealednessMacros$.exhaustive_impl(SealednessMacros.scala:29)
exhaustive[Parent](Seq(A, B))
^
one error found
Note that we need to help the compiler out with an explicit type parameter indicating the parent.
Update. Since 2.10.0-M7 we're exposing the methods mentioned in this answer as a part of public API. isSealed
is ClassSymbol.isSealed
and sealedDescendants
is ClassSymbol.knownDirectSubclasses
.
This is not going to be an answer to your question.
But, if you're willing to settle for something more like Enumeration.values()
, and you're using a recent milestone of 2.10, and you're willing to muck about with some ugly casting-to-internal-APIs business, you can write the following:
import scala.reflect.runtime.universe._
def sealedDescendants[Root: TypeTag]: Option[Set[Symbol]] = {
val symbol = typeOf[Root].typeSymbol
val internal = symbol.asInstanceOf[scala.reflect.internal.Symbols#Symbol]
if (internal.isSealed)
Some(internal.sealedDescendants.map(_.asInstanceOf[Symbol]) - symbol)
else None
}
Now if you've got a hierarchy like this:
object Test {
sealed trait Parent
case object A extends Parent
case object B extends Parent
case object C extends Parent
}
You can get the type symbols for the members of the sealed type hierarchy like this:
scala> sealedDescendants[Test.Parent] getOrElse Set.empty
res1: Set[reflect.runtime.universe.Symbol] = Set(object A, object B, object C)
It's hideous, but I don't think you're going to get what you actually want without writing a compiler plugin.