Sunday, August 4, 2013

A Voyage into the Scala Reflection land, part 2: case or no case

Let's make a little module (using :paste mode of the console or by compiling it separately and importing the corresponding module) - it looks a bit weird but suitable enough for what I want to demonstrate:

trait DemoStuff {
  lazy val i = 0 
  lazy val s = ""
  lazy val printMe = s"${s},${i}"
}

case object CaseEnglish extends DemoStuff {
  override lazy val i = 1 
  override lazy val s = "Hello"
}

case object CaseGerman extends DemoStuff {
  override lazy val i = 2 
  override lazy val s = "Guten TaG"
}

object NonCaseOne extends DemoStuff {
  override lazy val i = 3 
  override lazy val s = "Bye"
}

(Regarding the usage of lazy vals, I was inspired by this explanation).

In theory, both "normal" objects and case objects in Scala are supposed to be singletons. However, as far as serialization is concerned, it seems that case objects are more singletons than their non-case siblings

scala> val nco = NonCaseOne
nco: NonCaseOne.type = NonCaseOne$@74232853

scala> val ce = CaseEnglish
ce: CaseEnglish.type = CaseEnglish

This mimics the same conventions for the classes versus case classes.

Let's go on with reflection. Assume that, like in the previous case, we imported the universe, the current mirror and defined the helper to get the type tag for us.

scala> val ceType = getTypeTag(ce)
ceType: reflect.runtime.universe.Type = CaseEnglish.type

Let's look at the base classes for this type.

scala> val baseClasses = ceType.baseClasses
baseClasses: List[reflect.runtime.universe.Symbol] = List(object CaseEnglish, trait Serializable, trait Serializable, trait Product, trait Equals, trait DemoStuff, class Object, class Any)

I am still not sure why train Serializable is listed twice in these cases, but let's look at the symbol for DemoStuff, which is, as we know, the direct parent of our objects.

scala> val dsSymbol = baseClasses.filter(_.name.toString=="DemoStuff").head
res9: reflect.runtime.universe.Symbol = trait DemoStuff

Interesting to note that both ceType.typeSymbol and dsSymbol are seen as classes:

scala> ceType.typeSymbol.isClass
res12: Boolean = true

scala> dsSymbol.isClass
res14: Boolean = true

Apparently, there is a class implicitly defined for every object. We can get the symbols for the corresponding objects by asking for the companion of the original symbol, and see that one of them is considered to be a class and another isn't, also that one of them is described as "module" and another as "module class":

scala> val ceoSymbol = ceType.typeSymbol.companionSymbol
ceoSymbol: reflect.runtime.universe.Symbol = object CaseEnglish

scala> ceoSymbol.isClass
res16: Boolean = false

scala> ceoSymbol.isModule
res20: Boolean = true

scala> ceoSymbol.isModuleClass
res20: Boolean = false

scala> ceType.typeSymbol
res17: reflect.runtime.universe.Symbol = object CaseEnglish

scala> ceType.typeSymbol.isClass
res18: Boolean = true

scala> ceType.typeSymbol.isModule
res21: Boolean = false

scala> ceType.typeSymbol.isModuleClass
res21: Boolean = true

There is no companion symbol for a trait, however:

scala> dsSymbol.companionSymbol
res19: reflect.runtime.universe.Symbol = <none>

Now the question: if the dsSymbol is a class, can't we get its successors?

scala> val descendants = dsSymbol.asClass.knownDirectSubclasses
descendants: Set[reflect.runtime.universe.Symbol] = Set()

No luck? Now let's make the trait DemoStuff sealed, reload the example and do the same steps:

sealed trait DemoStuff {
  lazy val i = 0 
...(the rest of the code and the reflection steps are the same)...

scala> val dsSymbol = ru.typeOf[DemoStuff]
dsSymbol: reflect.runtime.universe.Type = DemoStuff

scala> val descendants = dsSymbol.asClass.knownDirectSubclasses
descendants: Set[reflect.runtime.universe.Symbol] = Set(object CaseEnglish, object CaseGerman, object NonCaseOne)

Yay, look what we've got! Just like with the good old enums, we can see them all, because for the sealed trait, all its direct descendants should be defined in the same module, so the parent class knows about them all.

Now suppose we want to instantiate all such objects. One way of using all this info would be to map the types and some inside information - e.g. val i in our case - so that we could mimic accessing the enums by value. Of course, if all elements are case objects, we can also just map the types to their string representations.

There is one right way and one wrong way to instantiate objects. Let's do the right way first.
In implies getting object companions and converting each of them from Symbol to ModuleSymbol:

scala> val descendantObjects = descendants map (m=> if (m.isModuleClass) m.companionSymbol else m) map (_.asModule)
descendantObjects: scala.collection.immutable.Set[reflect.runtime.universe.ModuleSymbol] = Set(object CaseEnglish, object CaseGerman, object NonCaseOne)

Finally, we can use the mirror to turn these symbols into instances:

scala> val reflectedObjects = descendantObjects map (m=>mirror.reflectModule(m).instance.asInstanceOf[DemoStuff])
reflectedObjects: scala.collection.immutable.Set[DemoStuff] = Set(CaseEnglish, CaseGerman, NonCaseOne$@74232853)

They do the right things, and they are equal to the "normally" created instances:

scala> reflectedObjects map (_.printMe)
res25: scala.collection.immutable.Set[String] = Set(Hello,1, Guten TaG,2, Bye,3)

scala> reflectedObjects.head == ce
res26: Boolean = true

Now, the wrong way! (DANGER!)
Suppose we go ahead with classes, not bothering to get the object companions, and try to instantiate them using constructors:

scala> val dsType = ru.typeOf[DemoStuff]
dsType: reflect.runtime.universe.Type = DemoStuff

scala> val descendants = dsType.typeSymbol.asClass.knownDirectSubclasses
descendants: Set[reflect.runtime.universe.Symbol] = Set(object CaseEnglish, object CaseGerman, object NonCaseOne)

scala> val ctors = descendants map (_.typeSignature.member(ru.nme.CONSTRUCTOR))
ctors: scala.collection.immutable.Set[reflect.runtime.universe.Symbol] = Set(constructor CaseEnglish, constructor CaseGerman, constructor NonCaseOne)

So let's reflect and instantiate these classes:

scala> val reflected = descendants map (m=>mirror.reflectClass(m.asClass))
reflected: scala.collection.immutable.Set[reflect.runtime.universe.ClassMirror] = Set(class mirror for CaseEnglish (bound to null), class mirror for CaseGerman (bound to null), class mirror for NonCaseOne (bound to null))

scala> val stuff = (reflected zip ctors) map (m=> m._1.reflectConstructor(m._2.asMethod))
stuff: scala.collection.immutable.Set[reflect.runtime.universe.MethodMirror] = Set(constructor mirror for CaseEnglish.<init>(): CaseEnglish.type (bound to null), constructor mirror for CaseGerman.<init>(): CaseGerman.type (bound to null), constructor mirror for NonCaseOne.<init>(): NonCaseOne.type (bound to null))

scala> val instances = stuff map (_.apply().asInstanceOf[DemoStuff])
instances: scala.collection.immutable.Set[DemoStuff] = Set(CaseEnglish, CaseGerman, NonCaseOne$@661a6677)

scala> instances map (_.printMe)
res1: scala.collection.immutable.Set[String] = Set(Hello,1, Guten TaG,2, Bye,3)

Looks about right? But... what if we compare the newly instantiated CaseEnglish with the our old friend val ce? They should be the same, right?

scala> instances.head == ce
res2: Boolean = false

OOPS. We managed to create those shadow classes for our, supposedly one and unique, case objects. Congratulations, what a bummer!

That concludes the story for now... but you'll never know what the future brings. Hope it was useful :)

No comments:

Post a Comment