Macro methods and subtypes
Suppose we want to define an HListable
trait in Scala that will add a
members
method returning an HList
of member values to any case
class that extends it. This would let us write the following, for
example:
scala> case class User(first: String, last: String, age: Int) extends HListable
defined class User
scala> val foo = User("Foo", "McBar", 25)
foo: User = User(Foo,McBar,25)
scala> foo.members == "Foo" :: "McBar" :: 25 :: HNil
res0: Boolean = true
So we try the following, which looks reasonable at a glance:
import scala.language.experimental.macros
import shapeless._
trait HListable {
def members: HList = macro HListable.members_impl[this.type]
}
object HListable {
import scala.reflect.macros.Context
def members_impl[A <: HListable: c.WeakTypeTag](c: Context): c.Expr[HList] = {
import c.universe._
weakTypeOf[A].declarations.collect {
case m: MethodSymbol if m.isCaseAccessor => m
}.foldRight(reify(HNil: HList)) {
case (m, acc) =>
val value = c.Expr(Select(c.resetAllAttrs(c.prefix.tree), m.name))
reify(value.splice :: acc.splice)
}
}
}
Unfortunately it doesn't actually work:
scala> foo.members
res0: shapeless.HList = HNil
The problem is that the this
in the type argument to the macro is resolved
too early—we don't get the kind of late binding we generally expect in Scala.
I'm using type-level lists in this example, but the problem we're running
into here is much more general, and
can turn up any time you're writing a macro method that needs information
about the type of a subtype of the class or trait it's defined in.
One solution would be to use F-bounded polymorphism:
trait HListable[A <: HListable[A]] {
def members: HList = macro HListable.members_impl[A]
}
This works...
scala> case class User(first: String, last: String, age: Int) extends HListable[User]
defined class User
scala> val foo = User("Foo", "McBar", 25)
foo: User = User(Foo,McBar,25)
scala> foo.members == "Foo" :: "McBar" :: 25 :: HNil
res0: Boolean = true
But ugh, that's a lot of complexity to add for this one little bit of functionality.
Another solution (seen in this Stack Overflow question, for example) is to add a type parameter to the method:
trait HListable {
def members[A <: HListable]: HList = macro HListable.members_impl[A]
}
This also works...
scala> foo.members[User] == "Foo" :: "McBar" :: 25 :: HNil
res0: Boolean = true
But it's annoying that we have to indicate that we're talking about a User
when the
compiler already knows that that's exactly what foo
is.
We can get exactly what we want, though, through the magic of implicit classes:
import scala.language.experimental.macros
import shapeless._
trait HListable
object HListable {
import scala.reflect.macros.Context
implicit class HListThisThing[A <: HListable](val a: A) extends AnyVal {
def members: HList = macro HListable.members_impl[A]
}
def members_impl[A <: HListable: c.WeakTypeTag](c: Context): c.Expr[HList] = {
import c.universe._
weakTypeOf[A].declarations.collect {
case m: MethodSymbol if m.isCaseAccessor => m
}.foldRight(reify(HNil: HList)) {
case (m, acc) =>
val value = c.Expr(
Select(
Select(c.resetAllAttrs(c.prefix.tree), newTermName("a")),
m.name
)
)
reify(value.splice :: acc.splice)
}
}
}
And now:
scala> foo.members == "Foo" :: "McBar" :: 25 :: HNil
res0: Boolean = true
See this Stack Overflow answer for another example of this trick in action.