Search code examples
scalamacrostype-inferenceshapeless

Infer HList type when building the list with a macro


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?


Solution

  • 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.