Chris Birchall
London Scala User Group
6th Dec 2016
Program
Data
Data
Side effects
Program
Program
Side effects
Program
foo.bar()Apply
Select
Ident
TermName("foo")
TermName("bar")
List()
Let's write a macro that doesn't do anything!
Pattern matching and code generation using quasiquotes
At compile time:
Hello {{firstName}}!
The weather is {{weather}} today.case class Context(firstName: String, weather: String)
Let's port the typechecked Mustache example
joy
misery
joy
misery
write some boilerplate code
joy
misery
write some boilerplate code
use runtime reflection
joy
misery
write some boilerplate code
use runtime reflection
generate source code
joy
misery
write some boilerplate code
use runtime reflection
generate source code
write a macro
joy
misery
write some boilerplate code
use runtime reflection
generate source code
write a macro
let Miles write the macro for you
case class Person(name: String, age: Int)String :: Int :: HNil("name", String) :: ("age", Int) :: HNilGeneric
LabelledGeneric
case class Foo(wow: String, yeah: Int)
val instance = Foo("hello", 123)
val map: Map[String, Any] = caseClassToMap(instance)
println(map) // Map("wow" -> "hello", "yeah" -> 123)import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
def caseClassToMap[A](a: A): Map[String, Any] =
  macro impl[A]
def impl[A: c.WeakTypeTag](c: Context)(a: c.Tree): c.Tree = {
  import c.universe._
  val A = weakTypeOf[A]
  if (!(A.typeSymbol.isClass && A.typeSymbol.asClass.isCaseClass))
    c.abort(c.enclosingPosition, "Sorry, case classes only")
  val primaryCtor = A.typeSymbol.asClass.primaryConstructor.asMethod
  val params = primaryCtor.paramLists.flatten
  val kvPairs = params.map { k =>
    q"${k.name.toString} -> $a.${k.name.toTermName}"
  }
  q"""_root_.scala.collection.immutable.Map($kvPairs: _*)"""
}
import shapeless._
import shapeless.ops.hlist.ToList
import shapeless.ops.record.Fields
def caseClassToMap[A, L <: HList, F <: HList](a: A)
  (implicit 
    generic: LabelledGeneric.Aux[A, L],
    fields: Fields.Aux[L, F],
    toList: ToList[F, (Symbol, Any)]
  ): Map[String, Any] = {
  val labelledGen = generic.to(a)
  val fieldsHlist = fields(labelledGen)
  toList(fieldsHlist)
    .map { case (symbol, value) => (symbol.name, value) }
    .toMap
}
case class Input(foo: Int, baz: String)
case class Output(foo: Int, bar: Double, baz: String)
val input = Input(123, "wow")
val output = transform[Output](input, "bar" -> 4.56)
println(output) // Output(123, 4.56, "wow")
def caseClassToCaseClass[A, B](a: A, extraParams: (String, Any)*): B =
  macro Bundle.impl[A, B]
class Bundle(val c: Context) {
  import c.universe._
  private case class TreeWithActualType(tree: Tree, actualType: Type)
  private def fail(msg: String) =
    c.abort(c.enclosingPosition, msg)
  def impl[A: c.WeakTypeTag, B: c.WeakTypeTag](a: c.Tree, extraParams: c.Expr[Tuple2[String, Any]]*): c.Tree = {
    val A = weakTypeOf[A]
    val B = weakTypeOf[B]
    if (!(A.typeSymbol.isClass && A.typeSymbol.asClass.isCaseClass))
      fail("Sorry, case classes only")
    if (!(B.typeSymbol.isClass && B.typeSymbol.asClass.isCaseClass))
      fail("Sorry, case classes only")
    val inputPrimaryCtor = A.typeSymbol.asClass.primaryConstructor.asMethod
    val inputParams = inputPrimaryCtor.paramLists.flatten
    val inputParamsMap: Map[TermName, TreeWithActualType] = inputParams.map { k =>
      val termName = k.name.toTermName
      termName -> TreeWithActualType(q"$a.$termName", k.info)
    }.toMap
    val extraParamsMap: Map[TermName, TreeWithActualType] = extraParams.map { expr =>
      val (key, value) = expr.tree match {
        case q"scala.Predef.ArrowAssoc[$_]($k).->[$_]($v)" => (k, v)
        case q"($k, $v)" => (k, v)
        case other => fail("You must pass extra params as either key -> value or (key, value)")
      }
      val q"${keyAsString: String}" = key
      val keyName = TermName(keyAsString)
      val actualValueType = expr.actualType.typeArgs(1)
      keyName -> TreeWithActualType(value.asInstanceOf[Tree], actualValueType)
    }.toMap
    val allParams: Map[TermName, TreeWithActualType] = inputParamsMap ++ extraParamsMap
    val outputPrimaryCtor = B.typeSymbol.asClass.primaryConstructor.asMethod
    val paramLists: List[List[Tree]] = 
      for (ps <- outputPrimaryCtor.paramLists) yield {
        for (p <- ps) yield {
          val termName = p.name.toTermName
          allParams.get(termName) match {
            case Some(t) if t.actualType weak_<:< p.typeSignature => t.tree
            case Some(t) => fail(s"Parameter ${termName.toString} has wrong type. Expected ${p.typeSignature} but got ${t.actualType}")
            case None => fail(s"Missing parameter of type ${termName.toString}")
          }
        }
      }
    q"new ${B.typeSymbol}(...$paramLists)"
  }
}import shapeless._
import shapeless.ops.hlist._
trait Transform[A, B, E <: HList] {
  def apply(a: A, extraFields: E): B
}
object Transform {
  implicit def genericTransform[
    A,
    B,
    ARepr         <: HList,
    BRepr         <: HList,
    CommonFields  <: HList,
    ExtraFields   <: HList,
    Unaligned     <: HList
  ](implicit
    aGen    : LabelledGeneric.Aux[A, ARepr],
    bGen    : LabelledGeneric.Aux[B, BRepr],
    inter   : Intersection.Aux[ARepr, BRepr, CommonFields],
    diff    : Diff.Aux[BRepr, CommonFields, ExtraFields],
    prepend : Prepend.Aux[ExtraFields, CommonFields, Unaligned],
    align   : Align[Unaligned, BRepr]
  ): Transform[A, B, ExtraFields] =
    new Transform[A, B, ExtraFields] {
      def apply(a: A, extra: ExtraFields): B = {
        val aRepr     = aGen.to(a)
        val common    = inter(aRepr)
        val unaligned = prepend(extra, common)
        val bRepr     = align(unaligned)
        bGen.from(bRepr)
      }
    }
}Also see Dave's talk at LSUG last month