Scala Language
macro's
Zoeken…
Invoering
Syntaxis
- def x () = macro x_impl // x is een macro, waarin x_impl wordt gebruikt om code te transformeren
- def macroTransform (annottees: Any *): Any = macro impl // Gebruik in annotaties om ze macro's te maken
Opmerkingen
Macro's zijn een taalfunctie die moet worden ingeschakeld, door scala.language.macros
importeren of met de compileroptie -language:macros
. Alleen macrodefinities vereisen dit; code die macro's gebruikt, hoeft dit niet te doen.
Macro-aantekening
Deze eenvoudige macro-annotatie voert het geannoteerde item uit zoals het is.
import scala.annotation.{compileTimeOnly, StaticAnnotation}
import scala.reflect.macros.whitebox.Context
@compileTimeOnly("enable macro paradise to expand macro annotations")
class noop extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro linkMacro.impl
}
object linkMacro {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
c.Expr[Any](q"{..$annottees}")
}
}
De annotatie @compileTimeOnly
genereert een fout met een bericht dat aangeeft dat de plug-in van de paradise
moet worden opgenomen om deze macro te gebruiken. Instructies om dit via SBT op te nemen zijn hier .
U kunt de hierboven gedefinieerde macro als volgt gebruiken:
@noop
case class Foo(a: String, b: Int)
@noop
object Bar {
def f(): String = "hello"
}
@noop
def g(): Int = 10
Methode Macro's
Wanneer een methode is gedefinieerd als een macro, neemt de compiler de code die wordt doorgegeven als zijn argument en verandert deze in een AST. Vervolgens wordt de macro-implementatie met die AST aangeroepen en wordt een nieuwe AST geretourneerd die vervolgens wordt teruggesplitst naar de aanroepsite.
import reflect.macros.blackbox.Context
object Macros {
// This macro simply sees if the argument is the result of an addition expression.
// E.g. isAddition(1+1) and isAddition("a"+1).
// but !isAddition(1+1-1), as the addition is underneath a subtraction, and also
// !isAddition(x.+), and !isAddition(x.+(a,b)) as there must be exactly one argument.
def isAddition(x: Any): Boolean = macro isAddition_impl
// The signature of the macro implementation is the same as the macro definition,
// but with a new Context parameter, and everything else is wrapped in an Expr.
def isAddition_impl(c: Context)(expr: c.Expr[Any]): c.Expr[Boolean] = {
import c.universe._ // The universe contains all the useful methods and types
val plusName = TermName("+").encodedName // Take the name + and encode it as $plus
expr.tree match { // Turn expr into an AST representing the code in isAddition(...)
case Apply(Select(_, `plusName`), List(_)) => reify(true)
// Pattern match the AST to see whether we have an addition
// Above we match this AST
// Apply (function application)
// / \
// Select List(_) (exactly one argument)
// (selection ^ of entity, basically the . in x.y)
// / \
// _ \
// `plusName` (method named +)
case _ => reify(false)
// reify is a macro you use when writing macros
// It takes the code given as its argument and creates an Expr out of it
}
}
}
Het is ook mogelijk om macro's te hebben die Tree
s als argumenten gebruiken. Zoals hoe reify
wordt gebruikt om Expr
s te maken, kunnen we met de q
(voor quasiquote) stringinterpolator Tree
s maken en deconstrueren. Merk op dat we hierboven q
hadden kunnen gebruiken ( expr.tree
is, verrassing, een Tree
zelf) ook, maar niet voor demonstratieve doeleinden.
// No Exprs, just Trees
def isAddition_impl(c: Context)(tree: c.Tree): c.Tree = {
import c.universe._
tree match {
// q is a macro too, so it must be used with string literals.
// It can destructure and create Trees.
// Note how there was no need to encode + this time, as q is smart enough to do it itself.
case q"${_} + ${_}" => q"true"
case _ => q"false"
}
}
Fouten in macro's
Macro's kunnen compilerwaarschuwingen en fouten activeren door het gebruik van hun Context
.
Stel dat we bijzonder overijverig zijn als het gaat om slechte code en we willen elk exemplaar van technische schuld markeren met een compilatie-infobericht (laten we niet nadenken over hoe slecht dit idee is). We kunnen een macro gebruiken die niets anders doet dan een dergelijk bericht uitzenden.
import reflect.macros.blackbox.Context
def debtMark(message: String): Unit = macro debtMark_impl
def debtMarkImpl(c: Context)(message: c.Tree): c.Tree = {
message match {
case Literal(Constant(msg: String)) => c.info(c.enclosingPosition, msg, false)
// false above means "do not force this message to be shown unless -verbose"
case _ => c.abort(c.enclosingPosition, "Message must be a string literal.")
// Abort causes the compilation to completely fail. It's not even a compile error, where
// multiple can stack up; this just kills everything.
}
q"()" // At runtime this method does nothing, so we return ()
}
Bovendien, in plaats van ???
om niet-geïmplementeerde code te markeren, kunnen we twee macro's maken, !!!
en ?!?
, die hetzelfde doel dienen, maar waarschuwen voor de compiler. ?!?
zal een waarschuwing veroorzaken, en !!!
zal een regelrechte fout veroorzaken.
import reflect.macros.blackbox.Context
def ?!? : Nothing = macro impl_?!?
def !!! : Nothing = macro impl_!!!
def impl_?!?(c: Context): c.Tree = {
import c.universe._
c.warning(c.enclosingPosition, "Unimplemented!")
q"${termNames.ROOTPKG}.scala.Predef.???"
// If someone were to shadow the scala package, scala.Predef.??? would not work, as it
// would end up referring to the scala that shadows and not the actual scala.
// ROOTPKG is the very root of the tree, and acts like it is imported anew in every
// expression. It is actually named _root_, but if someone were to shadow it, every
// reference to it would be an error. It allows us to safely access ??? and know that
// it is the one we want.
}
def impl_!!!(c: Context): c.Tree = {
import c.universe._
c.error(c.enclosingPosition, "Unimplemented!")
q"${termNames.ROOTPKG}.scala.Predef.???"
}