Scala Language
Macros
Recherche…
Introduction
Syntaxe
- def x () = macro x_impl // x est une macro, où x_impl est utilisé pour transformer le code
- def macroTransform (annottees: Any *): Any = macro impl // Utilisation dans les annotations pour les rendre macros
Remarques
Les macros sont une fonctionnalité de langage qui doit être activée, soit en important scala.language.macros
soit avec l'option de compilation -language:macros
. Seules les définitions de macro l'exigent. le code qui utilise des macros ne doit pas le faire.
Annotation Macro
Cette simple annotation de macro génère l'élément annoté tel quel.
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}")
}
}
L'annotation @compileTimeOnly
génère une erreur avec un message indiquant que le plugin paradise
compiler doit être inclus pour utiliser cette macro. Les instructions pour inclure ceci via SBT sont ici .
Vous pouvez utiliser la macro définie ci-dessus comme ceci:
@noop
case class Foo(a: String, b: Int)
@noop
object Bar {
def f(): String = "hello"
}
@noop
def g(): Int = 10
Méthode Macros
Lorsqu'une méthode est définie pour être une macro, le compilateur prend le code qui est passé en tant qu'argument et le transforme en AST. Il appelle ensuite l'implémentation de la macro avec cet AST, et renvoie un nouvel AST qui est ensuite renvoyé sur son site d'appel.
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
}
}
}
Il est également possible d'avoir des macros qui prennent Tree
s comme arguments. Comme comment reify
est utilisé pour créer Expr
s, l'interpolateur de chaînes q
(pour quasiquote) nous permet de créer et de déconstruire Tree
s. Notez que nous aurions pu utiliser q
ci-dessus ( expr.tree
is, surprise, un Tree
lui-même) également, mais pas à des fins démonstratives.
// 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"
}
}
Erreurs dans les macros
Les macros peuvent déclencher des avertissements et des erreurs de compilation à travers l'utilisation de leur Context
.
Disons que nous sommes particulièrement zélés en ce qui concerne le code erroné et que nous voulons marquer chaque instance de dette technique avec un message d'information de compilation (ne pensons pas à la gravité de cette idée). Nous pouvons utiliser une macro qui ne fait rien sauf émettre un tel message.
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 ()
}
De plus, au lieu d'utiliser ???
pour marquer du code non implémenté, nous pouvons créer deux macros, !!!
et ?!?
, qui servent le même objectif, mais émettent des avertissements du compilateur. ?!?
provoquera un avertissement, et !!!
causera une erreur pure et simple.
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.???"
}