How to find out whether a class is sealed?

I’d like to find out whether the sequence xs:Seq[Any] contains at least all the possible values of a given class ct:Class[_].

I think I need to find out two things. 1. is ct sealed? and 2. some way to collect or iterate over the instantiatable subclasses.

Can someone suggest a way to do this?

(this was asked on discord Discord, but maybe too complicated for discord )

probably scala-reflect library will help you. i’ve picked something mostly at random: Scala Reflection Library 2.13.8 - scala.reflect.api.Symbols.ClassSymbol maybe that will work for you?

def isSealed: Boolean
Does this symbol represent a sealed class?
Definition Classes
ClassSymbolApi

google “scala 3 mirror”. but enumerating subclasses using that mirrors is a compile-time operation and requires you to provide the root type statically. if you want a dynamic (i.e. not a statically provided type) and/or runtime solution then you’ll probably need classpath scanning and that is a pandora’s box.

well, that sounds you want to have a plain old java enum :slight_smile:

2 Likes

It’s not me choosing the values. The values will be given, and my function needs to determine whether they exhaust a class or not. For example, given the class classOf[java.lang.Boolean] and the values, true and false , it should determine that these given values exhaust the given class.

I’d think the best you could do is to come up with an Enumerable type class and implement this directly for the types of interest.

You could go with some macro based or reflection based approach for sealed traits with object children and somehow integrate this with the type class. You might even succeed extending this to case class children with Enumerable members, but this almost certainly will already require more of your life time than you’re willing to devote. And even then, you’d only have covered a very limited set of special cases. Most types will have an infinite domain, anyway, and of the few remaining types, the domain size will still exceed available memory.

In short, don’t go there. As @tarsa put it: Pandora’s box, whatever approach you choose.

Not sure if I understand what you mean by “types of interest”.

All “direct known subclasses”, which is well defined for a sealed trait/class.

I think that reply is missing a word. Typo?

“types of interest” ~ all types you require to support this behavior.

trait Enumerable[T]:
  def allValues: List[T]

given Enumerable[Boolean] with
  def allValues: List[Boolean] = List(true, false)

enum Color:
  case Red, Green, Blue

given Enumerable[Color] with
  def allValues: List[Color] = Color.values.toList

Starting with this, you could come up with some optional/add-on reflection/macro magic to generate the boilerplate for selected specific cases.

Ahh I see what you mean, but I don’t know of any way to predict which classes my customers/users will define.

Exactly. So they’d need to declare the corresponding Enumerable instances themselves, potentially perusing the reflection/macro machinery you provide if they’re lucky and it matches their case. But in general we’re back to “don’t go there”. :slight_smile:

that is for instances of classes. Answering the question, is this a list of all the possible instances of a class is impossible, except in the case of Boolean/true/false.
However, the other question is less difficult, right? Whether a class is sealed and if so whether the given set of classes exhausts the set of instantiatable subclasses.

Then @tarsa’s caveats (and then some) still apply.

Disclaimer: I’m not fluent with reflection nor with macros.

In Scala 2, given this…

sealed trait Color

object Color {
  case object Red extends Color
  case object Blue extends Color
  case object Yellow extends Color
}

…you could do something like this with reflection:

import scala.reflect.runtime.{universe => ru}

def enumerateSubtypes(tpe: ru.Type): Either[String, Set[String]] = {
  val sym = tpe.typeSymbol
  if (sym.isClass) {
    val classSym = sym.asClass
    if (classSym.isSealed)
      Right(classSym.knownDirectSubclasses.map(_.fullName))
    else
      Left(s"not sealed: $classSym")
  }
  else
    Left(s"not a class: $sym")
}

println(enumerateSubtypes(ru.typeOf[Boolean])) // Left(not sealed: class Boolean)
println(enumerateSubtypes(ru.typeOf[Color])) // Right(Set(foo.Color.Blue, foo.Color.Red, foo.Color.Yellow))

…or with macros:

import scala.reflect.macros.blackbox._
import scala.language.experimental.macros

def enumerateSubtypesImpl[T : c.WeakTypeTag](c: Context): c.Expr[Set[String]] = {
  import c.universe._
  val sym = c.mirror.weakTypeOf[T].typeSymbol
  if (sym.isClass) {
    val classSym = sym.asClass
    if (classSym.isSealed) {
      val subcs = classSym.knownDirectSubclasses.map(_.fullName)
      val setApply = Select(reify(Set).tree, TermName("apply"))
      c.Expr[Set[String]](Apply(setApply, subcs.toList.map(n => Literal(Constant(n)))))
    }
    else
      c.abort(c.enclosingPosition, s"not sealed: $classSym")
  }
  else
    c.abort(c.enclosingPosition, s"not a class: $sym")
}

def enumerateSubtypes[T]: Set[String] = macro enumerateSubtypesImpl[T]

// in another project(!):

//println(enumerateSubtypes[Boolean]) // compile error: "not sealed: class Boolean"
println(enumerateSubtypes[Color]) // Set(foo.Color.Red, foo.Color.Blue, foo.Color.Yellow)

However, this only covers a small subset of cases, I wouldn’t bet on this being foolproof, reflection/macros are complex, the closed world assumption to some extent applies in both scenarios, Strings probably won’t suffice, so you’ll either have to come up with your own types API or let reflection level types leak into the code, things likely work completely different in Scala 3, and so on. It’s a rabbit hole.

For Scala 3, you could try https://github.com/gzoller/scala-reflection. If you create an RType for sealed traits, you will get a subclass called SealedTraitInfo and that exposes the ‘children’ classes.