40

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 compile-time generated Iterable of the case objects deriving from a particular parent. Alternatively, I'd be happy with a way to make the compiler tell me I don't have all of the necessary types in some Iterable. I don't want a run-time, reflection-based approach.

As an example of the second approach, I'd like to have the following rough code generate a compile error where indicated.

sealed trait Parent
case object A extends Parent
case object B extends Parent
case object C extends Parent

// I want a compiler error here because C is not included in the Seq()
val m = Seq(A, B).map(somethingUseful)

Feel free to answer by telling me it's not possible. It just seems like it should be possible at some level because the compiler must be doing essentially the same work when determining a match is non-exhaustive.

Thinking about it another way, I'd take something like the Enumeration.values() method except applied to case objects. Certainly, I could add something similar to the code above with a manually maintained list of values to the parent's companion object, but that seems needlessly error-prone when the compiler could do that for me.

// Manually maintained list of values
object Parent { 
    val values = Seq(A, B, C)
}
Leif Wickland
  • 3,693
  • 26
  • 43

2 Answers2

26

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.

Eugene Burmako
  • 13,028
  • 1
  • 46
  • 59
Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Sealedness is not exposed? Have you discussed it with Eugene? – Daniel C. Sobral Aug 22 '12 at 23:36
  • 1
    Sealedness is exposed on the `ClassSymbol` through the method `isSealed`. You can get that by calling `asClass` on a `Symbol`. – Daniel C. Sobral Aug 22 '12 at 23:41
  • Ah! I now see what the problem might be. You need to write a macro -- that's a requirement of the question anyway, for practical purposes. On a macro, `isSealed` is there -- I didn't check on runtime, since Leif said he didn't want runtime reflection. – Daniel C. Sobral Aug 22 '12 at 23:43
  • I haven't had a chance to play with the new macro stuff yet, unfortunately. Is there a good reason the sealedness bits aren't exposed in the `runtime` API? – Travis Brown Aug 22 '12 at 23:46
  • I just checked and it is exposed there too. Though it seems not to be working. – Daniel C. Sobral Aug 23 '12 at 15:48
  • @TravisBrown, Thanks for the well thought out answer. I'm still on 2.9, but it's very interesting to see what's possible in 2.10. – Leif Wickland Aug 23 '12 at 19:07
  • @TravisBrown Out of curiosity, why'd you put the case objects into an enclosing object? Is that recommended in a style guide? It's not strictly required to make your code work is it? (I'm not criticizing; I'm just curious.) – Leif Wickland Aug 23 '12 at 19:09
  • @LeifWickland: I only included the enclosing object as a convenience for pasting into the REPL, where this code won't work without it (you could also use `:paste`). Otherwise no, it's not necessary. – Travis Brown Aug 23 '12 at 19:30
  • @TravisBrown Oh, right. I've seen that problem before with the REPL. Thanks for the explanation. – Leif Wickland Aug 23 '12 at 19:38
  • @DanielC.Sobral Would you be willing to provide an example of writing a macro to accomplish the Parent.values case I added to the end of my question, assuming that's possible? I'd love to see an example of how to apply macros to a problem I care about. – Leif Wickland Aug 23 '12 at 19:39
  • 1
    @LeifWickland It's not possible, unless you resort to the same trick Travis used -- `isSealed` is exposed, but not `sealedDescendants`. – Daniel C. Sobral Aug 23 '12 at 22:43
  • @DanielC.Sobral, ah, so you're saying even with macros the only solution would be a run-time one, right? – Leif Wickland Aug 24 '12 at 15:06
  • 1
    @LeifWickland: No, you should be able to get a compile-time solution with macros, but you'll still have to cast to the internal API. I'll take a shot at it this weekend. – Travis Brown Aug 24 '12 at 15:20
  • @TravisBrown That'd be much appreciated. Thanks! – Leif Wickland Aug 24 '12 at 19:08
  • @LeifWickland: See my new answer. – Travis Brown Aug 27 '12 at 01:07
  • how would I get the reference to the underlying object to call methods on it? – Edmondo Jan 13 '14 at 11:35
  • @TravisBrown I have the same question as Edmondo: how to call a method from those objects? – Rik Schaaf Apr 01 '18 at 20:20
  • @RikSchaaf Probably worth a new question? – Travis Brown Apr 03 '18 at 09:38
13

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.

Eugene Burmako
  • 13,028
  • 1
  • 46
  • 59
Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • 1
    You can use c.error and c.abort to report errors. The former just signalizes about an error to the compiler, while the latter calls c.error and then terminates the macro. – Eugene Burmako Aug 27 '12 at 08:13
  • 1
    Also in M7: 1) c.TypeTag needs to be changed to c.AbsTypeTag, 2) typeOf[T] needs to become implicitly[c.AbsTypeTag[T]].tpe, 3) there is the isSealed method. – Eugene Burmako Aug 27 '12 at 08:18
  • 1
    About casting. You can always drop me a line with a request to expose some internal APIs. Especially since reflection API is still not frozen, and the stuff we add, say, today will most likely end up in 2.10.0-final. – Eugene Burmako Aug 27 '12 at 08:31
  • Thanks for taking the time to work out and post this example, @TravisBrown. – Leif Wickland Aug 27 '12 at 15:05
  • 2
    Commit [9abf74be15672ce4ec1900a6b26fbf35cbce5866](9abf74be15672ce4ec1900a6b26fbf35cbce5866) introduced `knownDirectSubclasses`, which should suffice to avoid the cast. I have no idea if that went on to 2.10 or not. – Daniel C. Sobral Nov 06 '12 at 16:10