I want to generate aliases of methods using annotation macros in Scala 2.11+. I am not even sure that is even possible. If yes, how?
Example - Given this below, I want the annotation macros to expand into
class Socket {
#alias(aliases = Seq("!", "ask", "read"))
def load(n: Int): Seq[Byte] = {/* impl */}
}
I want the above to generate the synonym method stubs as follows:
class Socket {
def load(n: Int): Seq[Byte] = // ....
def !(n: Int) = load(n)
def ask(n: Int) = load(n)
def read(n: Int) = load(n)
}
The above is of course a facetious example but I can see this technique being useful to auto generate sync/async versions of APIs or in DSLs with lots of synonyms. Is it possible to also expose these generated methods in the Scaladoc too? Is this something possible using Scala meta?
Note: What I am asking is quite different from: https://github.com/ktoso/scala-macro-method-alias
Also please don't mark this as a duplicate of this as the question is a bit different and a lot has changed in Scala macro land in past 3 years.
This doesn't seem possible exactly as stated. Using a macro annotation on a class member does not allow you to manipulate the tree of the class itself. That is, when you annotate a method within a class with a macro annotation, macroTransform(annottees: Any*) will be called, but the only annottee will be the method itself.
I was able to get a proof-of-concept working with two annotations. It's obviously not as nice as simply annotating the class, but I can't think of another way around it.
You'll need:
import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
The idea is, you can annotate each method with this annotation, so that a macro annotation on the parent class is able to find which methods you want to expand.
class alias(aliases: String *) extends StaticAnnotation
Then the macro:
// Annotate the containing class to expand aliased methods within
#compileTimeOnly("You must enable the macro paradise plugin.")
class aliased extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro AliasMacroImpl.impl
}
object AliasMacroImpl {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val result = annottees map (_.tree) match {
// Match a class, and expand.
case (classDef # q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }") :: _ =>
val aliasedDefs = for {
q"#alias(..$aliases) def $tname[..$tparams](...$paramss): $tpt = $expr" <- stats
Literal(Constant(alias)) <- aliases
ident = TermName(alias.toString)
} yield {
val args = paramss map { paramList =>
paramList.map { case q"$_ val $param: $_ = $_" => q"$param" }
}
q"def $ident[..$tparams](...$paramss): $tpt = $tname(...$args)"
}
if(aliasedDefs.nonEmpty) {
q"""
$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
..$stats
..$aliasedDefs
}
"""
} else classDef
// Not a class.
case _ => c.abort(c.enclosingPosition, "Invalid annotation target: not a class")
}
c.Expr[Any](result)
}
}
Keep in mind this implementation will be brittle. It only inspects the annottees to check that the first is a ClassDef. Then, it looks for members of the class that are methods annotated with #alias, and creates multiple aliased trees to splice back into the class. If there are no annotated methods, it simply returns the original class tree. As is, this will not detect duplicate method names, and strips away modifiers (the compiler would not let me match annotations and modifiers at the same time).
This can easily be expanded to handle companion objects as well, but I left them out to keep the code smaller. See the quasiquotes syntax summary for the matchers I used. Handling companion objects would require modifying the result match to handle case classDef :: objDef :: Nil, and case objDef :: Nil.
In use:
#aliased
class Socket {
#alias("ask", "read")
def load(n: Int): Seq[Byte] = Seq(1, 2, 3).map(_.toByte)
}
scala> val socket = new Socket
socket: Socket = Socket#7407d2b8
scala> socket.load(5)
res0: Seq[Byte] = List(1, 2, 3)
scala> socket.ask(5)
res1: Seq[Byte] = List(1, 2, 3)
scala> socket.read(5)
res2: Seq[Byte] = List(1, 2, 3)
It can also handle multiple parameter lists:
#aliased
class Foo {
#alias("bar", "baz")
def test(a: Int, b: Int)(c: String) = a + b + c
}
scala> val foo = new Foo
foo: Foo = Foo#3857a375
scala> foo.baz(1, 2)("4")
res0: String = 34
Related
I'm learning how to write Scala Macros and wrote a macro annotation that removes an annotation from a type parameter of an annotated function. Here it is.
The annotation to remove:
class garbage extends StaticAnnotation
Implementation of macro to remove the annotation:
#compileTimeOnly("Compile-time only annotation")
class removeGarbage extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro impl
}
object removeGarbage {
def impl(c: whitebox.Context)(annottees: c.Tree*) = {
import c.universe._
println(annottees)
val expandee = annottees.toList collect {
case q"$mods def $templatename[..$typeparams](...$paramss): $tpt = $body" =>
val modifiedParams = typeparams collect {
case q"$mods type $name[..$args] = $tpt" =>
val modifiedMods = mods match {
case Modifiers(flags, privateWithin, annots) =>
Modifiers(flags, privateWithin, annots.filter(_ == q"new garbage()"))
}
q"$modifiedMods type $name[..$args] = $tpt"
}
q"$mods def $templatename[..$modifiedParams](...$paramss): $tpt = $body"
case annottee =>
c.abort(c.enclosingPosition, s"$annottee cannot be annotated with #removeGarbage. Only def methods are allowed")
}
println(expandee)
q"..$expandee"
}
}
Test method:
trait Test{
#removeGarbage
def someMethod[#garbage Source, G[_]](i: Int): G[List[Int]]
}
That seem to work fine. To check it I compared the log added with println(annottees) and println(expandees):
List(def someMethod[#new garbage() Source, G[_]](i: Int): G[List[Int]])
List(def someMethod[Source, G[_]](i: Int): G[List[Int]])
The problem about the solution is it looks difficult to read. Maybe I didn't use quasiquotes to their full potential. Is there a way to simplify the macro implementation (probably using quasiquotes more extensively...)?
That's ok for a macro code to be difficult to read :)
This is why metaprogramming shouldn't be the tool #1.
I can't see how your code can be reduced significantly.
You can replace
val modifiedMods = mods match {
case Modifiers(flags, privateWithin, annots) =>
Modifiers(flags, privateWithin, annots.filter(_ == q"new garbage()"))
}
with one-liner
val modifiedMods = mods.mapAnnotations(_.filter(_ == q"new garbage()"))
If you keep doing the same set of transformations in many macros you can similarly define helper methods like mapDef, mapTypeParams ...
If quasiquotes become too cumbersome you can consider to use ClassDef, Template, DefDef ... instead of quasiquotes or mix them with quasiquotes when convenient.
(Such questions are normally for https://codereview.stackexchange.com/ although metaprogramming seems to be not so popular there.)
Working in a codebase with scala that wants you, for certain classes, to define a sort of "make a new version" - so for instance if you have a class x(a :int, b:String, c:double)... it would have a function like this:
class x( a: Integer, b : String, c : Double) extends CanMakeNew
{
def newValue() = x( a, b, c)
}
I have no control over that - but would prefer not to implement it every time. Or, well... ever. Is there a way in scala, with reflection - to iterate over the constructor parameter values? I can use reflection to look at the parameter types - but as parameternames has not been turned on for this module, and I can't turn it on - I can't correlate those with the stored values in the class. Fundamentally, I'm looking for anyway of implementing a trait like:
trait CanMakeNewDoneForMe extends CanMakeNew {
def newValue() {I need the code that goes here}
So does scala reflection have any way of either inspecting the constructor or inspecting the object and seeing "ahh, this was the third parameter in the constructor"?
If you make X a case class it will have apply, copy... generated by compiler automatically.
And basically it’s not my codebase, so I can’t really change any of the shape of things...
When you make a class a case class you don't actually "change shape of things", you just add autogenerated methods.
Anyway, you can create a macro annotation that generates method newValue.
import scala.annotation.StaticAnnotation
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
class newValue extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro newValueMacro.impl
}
object newValueMacro {
def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._
annottees match {
case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: tail =>
val tparams1 = tparams.map {
case q"$_ type $name[..$_] >: $_ <: $_" => tq"$name"
}
val paramss1 = paramss.map(_.map {
case q"$_ val $pat: $_ = $_" => pat
})
q"""
$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
def newValue() = new $tpname[..$tparams1](...$paramss1)
..$stats
}
..$tail
"""
case _ => c.abort(c.enclosingPosition, "not a class")
}
}
}
#newValue
/*case*/ class X(a: Int, b : String, c : Double) {
override def toString: String = s"X($a, $b, $c)"
}
val x = new X(1, "a", 2.0) //X(1, a, 2.0)
// val x1 = x.copy()
val x1 = x.newValue() //X(1, a, 2.0)
I might be wrong here, but usually this is achieved using pattern matching and the apply() and unapply() methods defined in your companion object.
I've done a small test of your above code in a REPL session. I didn't understand the purpose of the newvalue() function so I skipped it.
class x(val a: Integer, val b : String, val c : Double)
{
//def newValue() = x( a, b, c)
}
object x {
def apply(a: Integer, b: String, c: Double): x = new x(a,b,c)
def unapply(m: x): Option[(Integer, String, Double)] = Some((m.a, m.b, m.c))
}
x(1, "hello", 99.0d) match {
case l: x => println(s"this the the 3rd arg in the constructor: ${l.c}")
}
The unapply() function def above allows the pattern match deconstruct on the object.
The alternative is to use a case class to define class x (this will define the apply() and unapply() functions for you).
I have some case classes which have a method tupled defined in its companion object. As it can be seen from the code below in companion objects, it is just code duplication.
case class Book(id: Int, isbn: String, name: String)
object Book {
def tupled = (Book.apply _).tupled // Duplication
}
case class Author(id: Int, name: String)
object Author {
def tupled = (Author.apply _).tupled // Duplication
}
From another question (can a scala self type enforce a case class type), it seems like we can not enforce the self-type of a trait to be a case class.
Is there a way to define a trait (say Tupled) that can be applied as following?
// What would be value of ???
trait Tupled {
self: ??? =>
def tupled = (self.apply _).tupled
}
// Such that I can replace tupled definition with Trait
object Book extends Tupled {
}
Because there's no relationship between FunctionN types in Scala, it's not possible to do this without arity-level boilerplate somewhere—there's just no way to abstract over the companion objects' apply methods without enumerating all the possible numbers of members.
You could do this by hand with a bunch of CompanionN[A, B, C, ...] traits, but that's pretty annoying. Shapeless provides a much better solution, which allows you to write something like the following:
import shapeless.{ Generic, HList }, shapeless.ops.product.ToHList
class CaseClassCompanion[C] {
def tupled[P <: Product, R <: HList](p: P)(implicit
gen: Generic.Aux[C, R],
toR: ToHList.Aux[P, R]
): C = gen.from(toR(p))
}
And then:
case class Book(id: Int, isbn: String, name: String)
object Book extends CaseClassCompanion[Book]
case class Author(id: Int, name: String)
object Author extends CaseClassCompanion[Author]
Which you can use like this:
scala> Book.tupled((0, "some ISBN", "some name"))
res0: Book = Book(0,some ISBN,some name)
scala> Author.tupled((0, "some name"))
res1: Author = Author(0,some name)
You might not even want the CaseClassCompanion part, since it's possible to construct a generic method that converts tuples to case classes (assuming the member types line up):
class PartiallyAppliedProductToCc[C] {
def apply[P <: Product, R <: HList](p: P)(implicit
gen: Generic.Aux[C, R],
toR: ToHList.Aux[P, R]
): C = gen.from(toR(p))
}
def productToCc[C]: PartiallyAppliedProductToCc[C] =
new PartiallyAppliedProductToCc[C]
And then:
scala> productToCc[Book]((0, "some ISBN", "some name"))
res2: Book = Book(0,some ISBN,some name)
scala> productToCc[Author]((0, "some name"))
res3: Author = Author(0,some name)
This will work for case classes with up to 22 members (since the apply method on the companion object can't be eta-expanded to a function if there are more than 22 arguments).
The problem is that the signature of apply differs from case to case, and that there is no common trait for these functions. Book.tupled and Author.tupled basically have the same code, but have very different signatures. Therefore, the solution may not be as nice as we'd like.
I can conceive of a way using an annotation macro to cut out the boilerplate. Since there isn't a nice way to do it with the standard library, I'll resort to code generation (which still has compile-time safety). The caveat here is that annotation macros require the use of the macro paradise compiler plugin. Macros must also be in a separate compilation unit (like another sbt sub-project). Code that uses the annotation would also require the use of the macro paradise plugin.
import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
#compileTimeOnly("enable macro paradise to expand macro annotations")
class Tupled extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro tupledMacroImpl.impl
}
object tupledMacroImpl {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val result = annottees map (_.tree) match {
// A case class with companion object, we insert the `tupled` method into the object
// and leave the case class alone.
case (classDef # q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
:: (objDef # q"object $objName extends { ..$objEarlyDefs } with ..$objParents { $objSelf => ..$objDefs }")
:: Nil if mods.hasFlag(Flag.CASE) =>
q"""
$classDef
object $objName extends { ..$objEarlyDefs } with ..$objParents { $objSelf =>
..$objDefs
def tupled = ($objName.apply _).tupled
}
"""
case _ => c.abort(c.enclosingPosition, "Invalid annotation target: must be a companion object of a case class.")
}
c.Expr[Any](result)
}
}
Usage:
#Tupled
case class Author(id: Int, name: String)
object Author
// Exiting paste mode, now interpreting.
defined class Author
defined object Author
scala> Author.tupled
res0: ((Int, String)) => Author = <function1>
Alternatively, something like this may be possible with shapeless. See #TravisBrown's better answer.
It seems like this doesn't work ( Using 2.11.1 and macro paradise 2.0.1).
I was hoping that the case class generated methods would either be suppressed, or be in the tree so I could get rid of it. Is this a hard limitation?
class evis extends StaticAnnotation {
def macroTransform(annottees: Any*) = macro EvisMacro.impl
}
object EvisMacro {
def impl(c: blackbox.Context)(annottees: c.Expr[Any]*) : c.Expr[Any] = {
import c.universe._
def makeApply(tpName: TypeName, parents: List[Tree], params: List[List[ValDef]] ) : List[Tree]= {
List(q"""def apply(...$params): $tpName = null""")
}
val result = annottees map (_.tree) match {
case (classDef # q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
:: Nil if mods.hasFlag(Flag.CASE) =>
c.info(c.enclosingPosition, s"Eviscerating $tpname !($mods, $parents, $paramss)", true)
parents match {
case q"${pname: TypeName}" :: rest =>
c.info(c.enclosingPosition, s"${pname.decodedName}", true )
val sc = c.universe.rootMirror.staticClass( pname.decodedName.toString )
c.info(c.enclosingPosition, s"${sc}", true )
}
val name = tpname.toTermName
q"""
$classDef
object $name {
..${makeApply(tpname, parents, paramss)}
}
"""
case (classDef # q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }")
:: q"object $objName {..$objDefs}"
:: Nil if mods.hasFlag(Flag.CASE) =>
q"""
$classDef
object $objName {
..${makeApply(tpname, parents, paramss)}
..$objDefs
}
"""
case _ => c.abort(c.enclosingPosition, "Invalid annotation target: must be a case class")
}
c.Expr[Any](result)
}
}
Using it:
trait Thing
#evis
case class Trade(id: Long, notional: Long, comment: String) extends Thing
#evis
case class Pop(name: String) extends Thing
object Pop{
}
object TestTrade extends App{
val t = Trade (1, 1, "")
val p : Pop = Pop("")
println(t)
}
Results in:
Error:(2, 2) method apply is defined twice
conflicting symbols both originated in file 'core/src/main/scala/Test.scala'
#evis
^
The problem is caused by the fact that, to the compiler, code generated by macro annotations isn't any different from code written by hand. If you manually write the code produced by the macro provided in the example, you'll get exactly the same double definition error, so it's not a bug - it's a limitation of case class synthesis. Unfortunately, case class synthesis is not extensible, so this needs to be worked around.
One workaround that I'd propose is erasing the CASE flag from the mods of the class, and then the macro can be completely free in choosing which members to generate. This, however, means that the macro will have to generate all the code that case classes typically generate, which is not going to be very pleasant. Another caveat here is that the compiler treats pattern matching over CASE classes specially by emitting somewhat more efficient code, so such an emulation would also lose some performance (I'd reckon that the loss will be miniscule, and probably even non-existent with the new name-based pattern matching mechanism from Scala 2.11 - but that would need to be tested).
I want to use macro annotations (macro-paradise, Scala 2.11) to generate synthetic traits within an annotated trait's companion object. For example, given some STM abstraction:
trait Var[Tx, A] {
def apply() (implicit tx: Tx): A
def update(value: A)(implicit tx: Tx): Unit
}
I want to define a macro annotation txn such that:
#txn trait Cell[A] {
val value: A
var next: Option[Cell[A]]
}
Will be re-synthesised into:
object Cell {
trait Txn[-Tx, A] {
def value: A
def next: Var[Option[Cell.Txn[Tx, A]]] // !
}
}
trait Cell[A] {
val value: A
var next: Option[Cell[A]]
}
I got as far as producing the companion object, the inner trait, and the value member. But obviously, in order for the next member to have the augmented type (instead of Option[Cell[A]], I need Option[Cell.Txn[Tx, A]]), I need to pattern match the type tree and rewrite it.
For example, say I find the next value definition in the original Cell trait like this:
case v # ValDef(vMods, vName, tpt, rhs) =>
How can I analyse tpt recursively to rewrite any type X[...] being annotated with #txn to X.Txn[Tx, ...]? Is this even possible, given like in the above example that X is yet to be processed? Should I modify Cell to mix-in a marker trait to be detected?
So the pattern matching function could start like this:
val tpt1 = tpt match {
case tq"$ident" => $ident // obviously don't change this?
case ??? => ???
}
I would like to preface my answer with the disclaimer that this kind of stuff is not easy to do in current Scala. In an ideal macro system, we would like to typecheck tpt in its lexical context, then walk through the structure of the resulting type replacing X[A] with X.Txn[Tx, A] for those X that are subtypes of TxnMarker, and then use the resulting type in the macro expansion.
However, this kind of happy mixing of untyped trees (that come into macro annotations) and typed trees (that typechecker emits) is incompatible with how compiler internals work (some details about that can be found in Scala macros: What is the difference between typed (aka typechecked) an untyped Trees), so we'll have to approximate.
This will be an approximation, because both in 2.10 and 2.11 our macros are unhygienic, meaning that they are vulnerable to name clashes (e.g. in the final expansion tree Var in Var[...] could bind to something unrelated if e.g. that trait we're rewriting contains a type member called Var). Unfortunately at the moment there's just one way to address this problem robustly, and it is very-very hard to carry out without deep understanding of compiler internals, so I'm not going into those details here.
import scala.reflect.macros.whitebox._
import scala.language.experimental.macros
import scala.annotation.StaticAnnotation
trait Var[T]
trait TxnMarker
object txnMacro {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
// NOTE: this pattern is only going to work with simple traits
// for a full pattern that captures all traits, refer to Denys's quasiquote guide:
// http://den.sh/quasiquotes.html#defns-summary
val q"$mods trait $name[..$targs] extends ..$parents { ..$stats }" = annottees.head.tree
def rewire(tpt: Tree): Tree = {
object RewireTransformer extends Transformer {
override def transform(tree: Tree): Tree = tree match {
case AppliedTypeTree(x # RefTree(xqual, xname), a :: Nil) =>
val dummyType = q"type SomeUniqueName[T] = $x[T]"
val dummyTrait = q"$mods trait $name[..$targs] extends ..$parents { ..${stats :+ dummyType} }"
val dummyTrait1 = c.typecheck(dummyTrait)
val q"$_ trait $_[..$_] extends ..$_ { ..${_ :+ dummyType1} }" = dummyTrait1
def refersToSelf = dummyTrait1.symbol == dummyType1.symbol.info.typeSymbol
def refersToSubtypeOfTxnMarker = dummyType1.symbol.info.baseClasses.contains(symbolOf[TxnMarker])
if (refersToSelf || refersToSubtypeOfTxnMarker) transform(tq"${RefTree(xqual, xname.toTermName)}.Txn[Tx, $a]")
else super.transform(tree)
case _ =>
super.transform(tree)
}
}
RewireTransformer.transform(tpt)
}
val stats1 = stats map {
// this is a simplification, probably you'll also want to do recursive rewiring and whatnot
// but I'm omitting that here to focus on the question at hand
case q"$mods val $name: $tpt = $_" => q"$mods def $name: $tpt"
case q"$mods var $name: $tpt = $_" => q"$mods def $name: Var[${rewire(tpt)}]"
case stat => stat
}
val annottee1 = q"$mods trait $name[..$targs] extends ..${parents :+ tq"TxnMarker"} { ..$stats }"
val companion = q"""
object ${name.toTermName} {
trait Txn[Tx, A] { ..$stats1 }
}
"""
c.Expr[Any](Block(List(annottee1, companion), Literal(Constant(()))))
}
}
class txn extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro txnMacro.impl
}
Actually, while I was writing this macro, I realized that it is ill-equipped to deal with cyclic dependencies of #txn-annotated definitions. The info.baseClasses.contains(symbolOf[TxnMarker]) check essentially forces expansion of the class/trait referred to in info, so the macro is going to loop. This won't lead to a SOE or a freeze though - scalac will just produce a cyclic reference error and bail out.
At the moment I've no idea how to address this problem purely with macros. Maybe you could leave the code generation part in an annotation macro and then move the type transformation part into a fundep materializer. Oh right, it seems that it might work! Instead of generating
object Cell {
trait Txn[-Tx, A] {
def value: A
def next: Var[Option[Cell.Txn[Tx, A]]]
}
}
trait Cell[A] {
val value: A
var next: Option[Cell[A]]
}
You could actually generate this:
object Cell {
trait Txn[-Tx, A] {
def value: A
def next[U](implicit ev: TxnTypeMapper[Option[Cell[A]], U]): U
}
}
trait Cell[A] {
val value: A
var next: Option[Cell[A]]
}
And TxnTypeMapper[T, U] could be summoned by a fundep materializer macro that would do the Type => Type transformation using Type.map, and that one won't lead to cyclic reference errors, because by the time a materializer is invoked (during typer), all macro annotations will have already expanded. Unfortunately, I don't have time to elaborate at the moment, but this looks doable!!