Expose type annotations in mirrors

Mirrors and inline methods present a significant improvement over macros for a great many reflective tasks. Unfortunately they don’t expose annotations on mirrored members.

This means that mirrors by themselves aren’t enough for a great many use-cases in which they would otherwise be a perfect solution; such as various forms of serialisation and persistence where annotations are commonly used to specify alternate names, identify primary keys, control field ordering, flag values as needing encryption, and other such tasks.

If product mirrors were able to expose annotations, this would eliminate one of the larger categories for which macros are still a painful necessity.

2 Likes

Could you provide a self contained example that shows the library, macro and user code. As well as some details on how you intend to access the mirror information.

So imagine a case class such as:

package my.pkg
case class Foo(
  a: Int,
  b: Double @deprecated("don't use this", "1.2")
)

That would give us:

ProductMirror {
  type MirroredLabel = "Foo"
  type MirrorredMonoType = my.pkg.Foo
  type MirroredElemLabels = ("a", "b")
  type MirroredElemTypes = (Int, Double)
}

Allowing only one annotation per value we could introduce a new AnnotationMirror:

AnnotationMirror {
  type MirroredLabel
  type MirroredMonoType
  type MirroredParamNames <: Tuple
  type MirroredParamTypes <: Tuple
  type MirroredParamValues <: Tuple
}

and a tuple of these be added as a new type on product mirrors, using Nothing or some equivalent to show the absence of an attribute:

ProductMirror {
  type MirroredLabel = "Foo"
  type MirrorredMonoType = my.pkg.Foo
  type MirroredElemLabels = ("a", "b")
  type MirroredElemTypes = (Int, Double)
  type MirroredElemAttributes = (
    Nothing,
    AnnotationMirror {
      type MirroredMonoType = scala.deprecated
      type MirroredLabel = "deprecated"
      type MirroredParamNames = ("message", "since")
      type MirroredParamTypes = (String, String)
      type MirroredParamValues = ("don't use this", "1.2")
    }
  )
}

Access would be via the same techniques we already use to work with mirrors

In practice there could be more than 1 annotation per field though. And there can be annotations on the class itself as well.

2 Likes

The MirroredParamValues looks suspicious. Mirrors do not give constructor arguments in general. This addition would make mirrors escape the safety of types.

Another concern is that there would not be an easy way to know which constructor of that annotation is used. Consider overloaded constructors and default parameters, this is already enough to complicate this beyond what is paractical/feasable.

@kevinwright here is the example of macro which is reading annotations on the class and the fields as well. I don’t know how this things looked in Scala 2, but in Scala 3 I wouldn’t say that it’s complicated or painful.

1 Like

True, but even handling just one annotation covers the overwhelming majority of cases. If multiples were needed then a tuple of tuples could be used:

type MirroredElemAttributes = (
  (),
  (
    AnnotationMirror {
      type MirroredMonoType = scala.deprecated
      type MirroredLabel = "deprecated"
      type MirroredParamNames = ("message", "since")
      type MirroredParamTypes = (String, String)
      type MirroredParamValues = ("don't use this", "1.2")
    }
  )
)

To quote from the scaladoc for StaticAnnotation:

A base class for static annotations. These are available to the Scala type checker or Scala reflection, even across different compilation units.

The truth is that static annotations require parameters to be literals and we are already dealing with something available at type level, so there’s no safety challenge here.

static annotations can have arbitrary arguments, represented as trees. ConstantAnnotation (on 2.13) enforces constant arguments.