I have a method taking an HList
and using it to build an instance of a class.
I would like to provide some simplified syntax, hiding the explicit cons. So I'd like to go from:
MyThingy.describe( 42 :: true :: "string" :: HNil)
to
MyThingy.describe {
42
true
"string"
}
where MyThingy
is defined like
class MyThingy[L <: HList](elems: L)
I've made an attempt with this macro
def describe[L <: HList](elements: Unit): MyThingy[L] = macro MyThingyMacros.describeImpl[L]
and
def describeImpl[L <: shapeless.HList : c.WeakTypeTag](c: Context)(elems: c.Tree): c.Tree = {
import c.universe._
def concatHList: PartialFunction[Tree, Tree] = {
case Block(l, _) =>
val els = l.reduceRight((x, y) => q"shapeless.::($x,$y)")
q"$els :: shapeless.HNil"
}
concatHList.lift(elems) match {
case None => c.abort(c.enclosingPosition, "BOOM!")
case Some(elemsHList) =>
val tpe = c.typecheck(elemsHList).tpe
q"new MyThingy[$tpe]($elemsHList)"
}
}
but the typechecker explodes:
exception during macro expansion: scala.reflect.macros.TypecheckException: inferred type arguments [Int,Boolean] do not conform to method apply's type parameter bounds [H,T <: shapeless.HList]
Apparently the compiler is trying to infer [Int, Boolean]
from the block before the macro expansion. I also don't understand why it requires two parameters, where describe
and MyThing
only require one.
Is there a way to have type inference driven by the tree produced by the macro?
I'm going to respectfully disagree with Miles a bit here. I personally can't stand auto-tupling, and if you want to use -Xlint
in your project, the solution in his answer is going to cause a lot of warning noise. I definitely agree that you should avoid macros when there's a viable alternative, but if I had to choose between auto-tupling and a macro in a case where I'm just providing syntactic sugar, I'd go with the macro.
In your case this isn't too hard—there's just a minor error (well, two, really) in your logic. The following will work just fine:
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
import shapeless._
class MyThingy[L <: HList](val elems: L)
def describeImpl[L <: HList: c.WeakTypeTag](c: Context)(elems: c.Tree) = {
import c.universe._
def concatHList: PartialFunction[Tree, Tree] = {
case Block(statements, last) =>
statements.foldRight(q"$last :: shapeless.HNil")(
(h, t) => q"shapeless.::($h, $t)"
)
}
concatHList.lift(elems) match {
case None => c.abort(c.enclosingPosition, "BOOM!")
case Some(elemsHList) =>
val tpe = c.typecheck(elemsHList).tpe
q"new MyThingy[$tpe]($elemsHList)"
}
}
def describe[L <: HList](elems: Any): MyThingy[L] = macro describeImpl[L]
Or more concisely:
def describeImpl[L <: HList: c.WeakTypeTag](c: Context)(elems: c.Tree) = {
import c.universe._
elems match {
case q"{ ..$elems }" =>
val hlist = elems.foldRight[c.Tree](q"shapeless.HNil: shapeless.HNil")(
(h, t) => q"shapeless.::($h, $t)"
)
q"new MyThingy($hlist)"
case _ => c.abort(c.enclosingPosition, "BOOM!")
}
}
The biggest issue was just in the reduction—you need to start with the HNil
, not build up a meaningless intermediate thing and then tack it on. You also need to capture the block's expression, and type it as Any
instead of Unit
to avoid value discarding.
(As a side note, I'm a little surprised this works as a whitebox macro, but as of 2.11.2 it does.)
I personally prefer this syntax with commas, though, and that's also pretty easy:
def describeImpl[L <: HList: c.WeakTypeTag](c: Context)(elems: c.Tree*) = {
import c.universe._
val hlist = elems.foldRight[c.Tree](q"shapeless.HNil: shapeless.HNil")(
(h, t) => q"shapeless.::($h, $t)"
)
q"new MyThingy($hlist)"
}
def describe[L <: HList](elems: Any*): MyThingy[L] = macro describeImpl[L]
The usage here is the same as with the product solution, but there's no auto-tupling involved.