Design choice: reflection, ValDef and synthetic getter


#1

Hi,

Answering a StackOverflow question, I found out about what I consider, without further context, a pretty strange design choice. Here’s some code demonstrating it:

scala> import scala.annotation.StaticAnnotation
import scala.annotation.StaticAnnotation

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._

scala> class Max(val value : Int) extends StaticAnnotation
defined class Max

scala>   class Child {
     |     @Max(5) val myMember = 2
     |   }
defined class Child

scala> val cls = classOf[Child]
cls: Class[Child] = class Child

scala> val mirror = runtimeMirror(cls.getClassLoader)
mirror: reflect.runtime.universe.Mirror = JavaMirror with... (I truncated this part which was super long and not useful)

scala> mirror.classSymbol(cls).selfType.decls
res0: reflect.runtime.universe.MemberScope = SynchronizedOps(constructor Child, value myMember, value myMember)

scala> println(mirror.classSymbol(cls).selfType.decls)
Scope{
  def <init>: <?>;
  val myMember: <?>;
  private[this] val myMember: scala.Int
}

scala> mirror.classSymbol(cls).selfType.decls.map(_.annotations)
res2: Iterable[List[reflect.runtime.universe.Annotation]] = List(List(), List(), List(Max(5)))

scala> mirror.classSymbol(cls).selfType.decls.map(_.isMethod)
res4: Iterable[Boolean] = List(true, true, false)

scala> mirror.classSymbol(cls).selfType.decls.map(_.asTerm.name)
res15: Iterable[reflect.runtime.universe.TermName] = List(<init>, myMember, myMember )

We can see that the name of the synthetic getter generated by the compiler is “myMember” as expected, but the name of the real, private attribute contains a space at the end, presuambly for deduping the declaration names in order to look them up. I wonder how and why such an arbitrary and seemingly undocumented choice could have been made.

Also, why is it not easier to directly find all the methods and, separately, all the fields ? Is there an easier way than my naive attempt at doing it ?

Thanks in advance !


#2

Hey @Dici,

Good question.

To get getters:

mirror.classSymbol(cls).selfType.decls.filter(_.asTerm.isGetter)

To get the original val definitions (turned into private members):

mirror.classSymbol(cls).selfType.decls.filter(_.asTerm.isVal)

The trick is to use the domain-specific methods for term symbols. By default, normal symbols don’t allow you to check this without converting them to term symbols.

We can see that the name of the synthetic getter generated by the compiler is “myMember” as expected, but the name of the real, private attribute contains a space at the end, presuambly for deduping the declaration names in order to look them up.

I don’t know why this strategy was introduced, but it does seem a pretty obvious design decision. Desugarings have to create getters and you cannot have a clashing name in the same scope.

Perhaps a Scala compiler maintainer can elaborate on it.


#3

I think the only person who can explain this design decision is @xeno-by.


#4

Hi @jvican, thanks for your answer. Yeah, I guess it’s not too bad when you know the type hierarchy well, but even after filtering if you want to get myMember you still need to add a space, which I find very sad. This is a very unexpected notation to me. Anything else than a space would have been at least barely acceptable (like an underscore as the first character, that’s a pretty common notation for attributes in some languages). As someone who knows Java basic reflection quite well, I find Scala’s API much more complicated without diving into it too much. Perhaps a lot of it is due to the fact that Scala has a much more complex type system and OOP model, but still, this type of design choices just make it more confusing to my opinion.

I don’t mean to be rude with this criticism, I’m just giving my genuine opinion after playing with the API for a short amount of time. Maybe it’s simply that the ramp-up time is bigger than what I expected.


#5

The trailing space should have been strictly compiler internal. It is never emitted in bytecode. It is indeed for de-duplicating a field from a getter of that field, because Scala would otherwise treat accesses to either as an ambiguous access to an overloaded member.

I believe the trailing space convention was chosen specifically to be almost invisible, so that it would not show up in error messages and confuse users. The problem is that with the advent of macros these compiler internals got exposed. They should not have been, and I believe scala.meta will do a much better job at hiding such things.


#6

Thanks for your answer Martin, that makes a lot of sense now. Just a precision, I was doing runtime reflection, so this is not only exposed in macros which I believe is a term reserved to compile time reflection. I will read about scala.meta, at the moment I have no idea what this is :slight_smile:

Thanks again


#7

Thanks for clarifying. Yes, runtime reflection has the same problem since it ultimately reads the same classfile attribute in which the members of a class as seen from Scala are enumerated.