Can I get a compile-time list of all of the case objects which derive from a sealed parent in Scala?

前端 未结 2 959
醉梦人生
醉梦人生 2020-11-27 03:38

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

相关标签:
2条回答
  • 2020-11-27 03:47

    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.

    0 讨论(0)
  • 2020-11-27 04:14

    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.

    0 讨论(0)
提交回复
热议问题