Scala 3 Macro: Get a list of all direct child objects of a sealed trait

I am trying to migrate a piece of scala.reflect code to Scala 3.

Given


sealed trait MyReader
object ReaderA extends MyReader
object ReaderB extends MyReader

How do I write a macro (or inline def?)

inline def findSubclassModulesOfSealedTrait[T]: List[T] = ???

// caller code
val mods = findSubclassModulesOfSealedTrait[MyReader]
// mods = List(ReaderA, ReaderB)

I get to the point where I have a List of all the Symbols and am completely at a loss here:

import scala.quoted.*

object FindClassesMacro {
  def findDirectSubModulesImpl[T](using
      quotes: Quotes,
      t: Type[T]
  ): Expr[Set[T]] = {
    import quotes.reflect.*

    val tpeSym                 = TypeTree.of[T].symbol
    val children: List[Symbol] = tpeSym.children
    val nonModules             = children.filterNot(_.flags.is(Flags.Module))
    val modules                = children.filter(_.flags.is(Flags.Module))

    // TODO
    '{Set.empty}

I have the feeling that this is super simple and that I am missing something trivial, any hints or examples to get me started are highly appreciated.

1 Like

I think you should be able to reuse the Mirror infrastructure. Type Class Derivation | Scala 3 Language Reference | Scala Documentation

1 Like

Thanks a lot for driving me off the macro path, this simplifies everything a lot!

It still took me some time to figure out how I get from the type t to the value (object instance) - all the examples summon a given of the type.

This is what I’ve got now:


inline def findSubclassModulesOfSealedTrait[T](using
        m: scala.deriving.Mirror.SumOf[T]
    ): Set[T] = 
      allInstances[m.MirroredElemTypes, m.MirroredType].toSet
    
inline def allInstances[ET <: Tuple, T]: List[T] =
      import scala.compiletime.*

      inline erasedValue[ET] match
        case _: EmptyTuple => Nil
        case _: (t *: ts)  => summonInline[ValueOf[t]].value.asInstanceOf[T] :: allInstances[ts, T]

It seems to work in this test-scastie but is it “safe” or are there loopholes leading to runtime exceptions?

4 Likes

This is pretty cool because it works without any dependencies in Scala 3 on jvm and js (all major browsers that I tested).

At the moment, it seems to be restricted to case objects. Adding a case class D to your example gives a compile error.

Did you make any progress on this?
Thanks a lot in advance.

2 Likes

I’m not really sure but I’d guess that it’s impossible to summon “the” instance of the case class (as there isn’t a single, unambiguous instance that can be summoned).

Unfortunately, I haven’t really dived into metaprogramming any more than required for what I have above, so I fear I can’t help you out here.

Thanks for your reply.

I think it is such a common use case to get a list of all child 'ClassTag’s for a given sealed trait/abstract class. It would save me so much error prone boilerplate. There are options, but only on JVM. I need something that also works in scala.js in a browser. If I find some solution I will post it here.

You can easily do that with a few small adjustments to the code of @cestLaVie

I know this works in the JVM. Does it work in Scala.js and/or Scala Native?

I’m not sure what you mean by “works”, but Scala.js does have support for ClassTags and Classes.

Wow. Thanks a lot @Jasper-M. Your scastie helped me to solve my problem.

tldr;
I adopted the code to compile with strict Scala 3 (? for _) and changed the names.
The context is an eventsourcing/cqrs framework built with Scala 3/JS (in browsers) using ZIO2, io.bullet.borer.Cbor, Skunk, Postgres and a few more. A fundamental idea is to use the same domain logic in the browser as on server side. In between is a transparent RPC mechanism (websockets) based on “registered” codecs for domain-specific objects like entities, events, completions, errors. Users can define them in custom domains, but they need to register the codecs. This is probably standard practice e. g. in Java EE, but the challenge was to make it work in jvm and js, if possible without dependencies. With this in place, there is no need for an additional transport layer, e.g. a REST api.

Now boilerplate is reduced a lot and it is much more robust to changes:

val live = ZLayer.scoped(makeLayer)

private def makeLayer = for
  codecregistry <- ZIO.service[CodecRegistry]
  result <- ZIO.fromAutoCloseable(make(codecregistry).register) // register is called on first usage
yield result

inline private def make(codecRegistry: CodecRegistry): Errors = new:
    def register =
      // given Codec[NotWorkable] = deriveCodec[NotWorkable]
      // given Codec[NotInSequence] = deriveCodec[NotInSequence]
      // given Codec[AggregateNotFound] = deriveCodec[AggregateNotFound]
      // given Codec[RegistrationError] = deriveCodec[RegistrationError]
      // given Codec[EncodingError] = deriveCodec[EncodingError]
      // given Codec[DecodingError] = deriveCodec[DecodingError]
      // given Codec[NoResult] = deriveCodec[NoResult]
      // given Codec[ServerSideError] = deriveCodec[ServerSideError]
      // given Codec[ClientSideError] = deriveCodec[ClientSideError]
      given Codec[EventstoreError] = deriveAllCodecs[EventstoreError]

      for 
        _ <- codecRegistry.registerSealed[EventstoreError]
        // _ <- codecRegistry.register[NotWorkable]
        // _ <- codecRegistry.register[NotInSequence]
        // _ <- codecRegistry.register[AggregateNotFound]
        // _ <- codecRegistry.register[RegistrationError]
        // _ <- codecRegistry.register[EncodingError]
        // _ <- codecRegistry.register[DecodingError]
        // _ <- codecRegistry.register[NoResult]
        // uups, I forgot ClientSideError
        // _ <- codecRegistry.register[ServerSideError]
      yield this

This is implemented with this (concrete) class:

It used to be a trait, but I ran into the problem described here: Abstract inline methods

class CodecRegistry private ():

  final transparent inline def registerSealed[A: Encoder: Decoder: ClassTag: Mirror.Of] =
    ZIO.succeed {
      val classname = classTag[A].runtimeClass.getName.nn
      if registeredCodecs.contains(classname) then println(s"already registered: $classname") 
      else
        sealedChildren[A].foreach(c => registerClass(c.runtimeClass.getName.nn))
        registerClass(classname)
    }
    .catchAllDefect(e => ZIO.die(RegistrationError(s"registration failed: ${e.getClass}(${e.getMessage})")))

  private final transparent inline def sealedChildren[A](using m: Mirror.Of[A]): Seq[ClassTag[? <: A]] =
    sealedInstances[m.MirroredElemTypes, m.MirroredType]

  private final transparent inline def sealedInstances[T <: Tuple, A]: Seq[ClassTag[? <: A]] =
    import scala.compiletime.*
    inline erasedValue[T] match
      case _: EmptyTuple => Nil
      case _: (a *: as)  => summonInline[ClassTag[a]].asInstanceOf[ClassTag[? <: A]] +: sealedInstances[as, A]

...

end CodecRegistry

This works for case class/object children of a sealed trait, but not for normal classes. These, however, can simply be registered manually.

1 Like

Thank you for this gist. If you remove the .toSet[T] in findSubclassModulesOfSealedTrait it will (obviously) return a List[T]. Will the order of this list always be equal to the order of definition in the source code?