Meta-program and/or shapeless all the things!
Chris Birchall
London Scala User Group
6th Dec 2016
We're hiring!
Programming
Program
Data
Data
Side effects
Metaprogramming
Program
Program
Side effects
Program
Scala macro
-
Function that runs at compile time
-
Input and output are trees (ASTs)
foo.bar()
Apply
Select
Ident
TermName("foo")
TermName("bar")
List()
=
Hello World
Let's write a macro that doesn't do anything!
Level up
Pattern matching and code generation using quasiquotes
Typechecked Mustache
At compile time:
- Load a Mustache template
- Parse it to find all the {{placeholders}}
- Generate a corresponding case class
Hello {{firstName}}!
The weather is {{weather}} today.
case class Context(firstName: String, weather: String)
scala.meta
Syntactic API
- Tokens, trees
- Quasiquotes
- Available now
Semantic API
- Symbols, name resolution, typechecking?, ...
- Work in progress
- See Eugene Burmako's scalax talk
meta paradise
inline/meta
- inline keyword replaces @inline annotation
- meta demarcates code that executes at compile time
- See SIP-28 & 29 for the gory details
meta paradise
Let's port the typechecked Mustache example
IntelliJ support
tutorial
Spectrum of Scala developer happiness
joy
misery
Spectrum of Scala developer happiness
joy
misery
write some boilerplate code
Spectrum of Scala developer happiness
joy
misery
write some boilerplate code
use runtime reflection
Spectrum of Scala developer happiness
joy
misery
write some boilerplate code
use runtime reflection
generate source code
Spectrum of Scala developer happiness
joy
misery
write some boilerplate code
use runtime reflection
generate source code
write a macro
Spectrum of Scala developer happiness
joy
misery
write some boilerplate code
use runtime reflection
generate source code
write a macro
let Miles write the macro for you
shapeless
case class Person(name: String, age: Int)
String :: Int :: HNil
("name", String) :: ("age", Int) :: HNil
Generic
LabelledGeneric
Convert case class to Map
goal
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)
Convert case class to Map
with a macro
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: _*)"""
}
Convert case class to Map
with shapeless
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
}
Convert one case class to another
goal
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")
Convert one case class to another
with a macro
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)"
}
}
Convert one case class to another
with shapeless
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)
}
}
}
learning shapeless
Also see Dave's talk at LSUG last month
Summary
- Macros are fun and useful
- Do the scala.meta tutorial
- Read the shapeless book
Meta-program and/or shapeless all the things! (LSUG)
By Chris Birchall
Meta-program and/or shapeless all the things! (LSUG)
- 3,367