Ausgabe
Ich versuche, case class
aus einem Gegebenen einen zu erzeugen, der aus den Feldern case class
streift . Option
Dies muss rekursiv erfolgen. Wenn also das Feld selbst ein case class
ist, muss Option
es auch aus seinen Feldern entfernt werden.
Bisher habe ich es geschafft, wo keine Felder sind, keine case class
. Aber für die Rekursion muss ich das ClassTag
für das Feld bekommen, wenn es ein case class
. Aber ich habe keine Ahnung, wie ich das machen kann. Anscheinend kann ich nur auf den Syntaxbaum vor der Typprüfung zugreifen (ich denke, es ist sinnvoll, wenn man bedenkt, dass der endgültige Quellcode noch nicht erstellt ist). Aber ich frage mich, ob es möglich ist, dies auf irgendeine Weise zu erreichen.
Hier ist mein Code und der fehlende Teil als Kommentar.
import scala.annotation.StaticAnnotation
import scala.collection.mutable
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros
import scala.annotation.compileTimeOnly
class RemoveOptionFromFields extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro RemoveOptionFromFields.impl
}
object RemoveOptionFromFields {
def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
def modifiedClass(classDecl: ClassDef, compDeclOpt: Option[ModuleDef]) = {
val result = classDecl match {
case q"case class $className(..$fields) extends ..$parents { ..$body }" =>
val fieldsWithoutOption = fields.map {
case ValDef(mods, name, tpt, rhs) =>
tpt.children match {
case List(first, second) if first.toString() == "Option" =>
// Check if `second` is a case class?
// Get it's fields if so
val innerType = tpt.children(1)
ValDef(mods, name, innerType, rhs)
case _ =>
ValDef(mods, name, tpt, rhs)
}
}
val withOptionRemovedFromFieldsClassDecl = q"case class WithOptionRemovedFromFields(..$fieldsWithoutOption)"
val newCompanionDecl = compDeclOpt.fold(
q"""
object ${className.toTermName} {
$withOptionRemovedFromFieldsClassDecl
}
"""
) {
compDecl =>
val q"object $obj extends ..$bases { ..$body }" = compDecl
q"""
object $obj extends ..$bases {
..$body
$withOptionRemovedFromFieldsClassDecl
}
"""
}
q"""
$classDecl
$newCompanionDecl
"""
}
c.Expr[Any](result)
}
annottees.map(_.tree) match {
case (classDecl: ClassDef) :: Nil => modifiedClass(classDecl, None)
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedClass(classDecl, Some(compDecl))
case _ => c.abort(c.enclosingPosition, "This annotation only supports classes")
}
}
}
Lösung
Ich bin mir nicht sicher, ob ich verstehe, welche Art von Rekursion Sie benötigen. Angenommen, wir haben zwei Fallklassen: die 1. kommentierte (bezieht sich auf die 2.) und die 2. nicht kommentierte
@RemoveOptionFromFields
case class MyClass1(mc: Option[MyClass2])
case class MyClass2(i: Option[Int])
Was soll das Ergebnis sein?
Derzeit verwandelt sich die Anmerkung in
case class MyClass1(mc: Option[MyClass2])
object MyClass1 {
case class WithOptionRemovedFromFields(mc: Class2)
}
case class MyClass2(i: Option[Int])
Wenn das Feld selbst eine Fallklasse ist, muss Option auch aus seinen Feldern entfernt werden.
Makroannotationen können nur Klassen und ihre Begleiter umschreiben, sie können keine anderen Klassen umschreiben. In meinem Beispiel mit 2 Klassen kann die Anmerkung MyClass1
und ihr Begleiter geändert werden, aber sie kann MyClass2
oder ihr Begleiter nicht neu geschrieben werden. Denn das MyClass2
sollte selbst kommentiert werden.
In einem Geltungsbereich werden Makroanmerkungen vor der Typprüfung dieses Geltungsbereichs expandiert. Beim Umschreiben sind die Bäume also untypisiert. Wenn Sie einige Bäume eingeben müssen (damit Sie ihre Symbole finden können), können Sie verwendenc.typecheck
Um zu überprüfen, ob eine Klasse eine Fallklasse ist, die Sie verwenden könnensymbol.isClass && symbol.asClass.isCaseClass
Wie kann man überprüfen, ob ein T zur Kompilierzeit in Scala eine Fallklasse ist?
Sie brauchen kaum ClassTag
s.
Eine weitere Komplikation ist, wenn MyClass1
und MyClass2
sich im selben Bereich befinden
@RemoveOptionFromFields
case class MyClass1(mc: Option[MyClass2])
case class MyClass2(i: Option[Int])
Dann wird bei der Erweiterung der MyClass1
Makroannotation für den Bereich noch keine Typprüfung durchgeführt, sodass es unmöglich ist, den Baum der Felddefinition zu prüfen mc: Option[MyClass2]
(die Klasse MyClass2
ist noch nicht bekannt). Wenn sich die Klassen in unterschiedlichen Bereichen befinden, ist dies in Ordnung
{
@RemoveOptionFromFields
case class MyClass1(mc: Option[MyClass2])
}
case class MyClass2(i: Option[Int])
Dies ist eine modifizierte Version Ihres Codes (ich drucke nur die Felder der zweiten Klasse)
import scala.annotation.StaticAnnotation
import scala.reflect.macros.blackbox
import scala.language.experimental.macros
import scala.annotation.compileTimeOnly
@compileTimeOnly("enable macro annotations")
class RemoveOptionFromFields extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro RemoveOptionFromFields.impl
}
object RemoveOptionFromFields {
def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
import c.universe._
def modifiedClass(classDecl: ClassDef, compDeclOpt: Option[ModuleDef]) = {
classDecl match {
case q"$mods class $className[..$tparams] $ctorMods(..$fields) extends { ..$earlydefns } with ..$parents { $self => ..$body }"
if mods.hasFlag(Flag.CASE) =>
val fieldsWithoutOption = fields.map {
case field@q"$mods val $name: $tpt = $rhs" =>
tpt match {
case tq"$first[..${List(second)}]" =>
val firstType = c.typecheck(tq"$first", mode = c.TYPEmode, silent = true) match {
case EmptyTree => println(s"can't typecheck $first while expanding @RemoveOptionFromFields for $className"); NoType
case t => t.tpe
}
if (firstType <:< typeOf[Option[_]].typeConstructor) {
val secondSymbol = c.typecheck(tq"$second", mode = c.TYPEmode, silent = true) match {
case EmptyTree => println(s"can't typecheck $second while expanding @RemoveOptionFromFields for $className"); NoSymbol
case t => t.symbol
}
if (secondSymbol.isClass && secondSymbol.asClass.isCaseClass) {
val secondClassFields = secondSymbol.typeSignature.decls.toList.filter(s => s.isMethod && s.asMethod.isCaseAccessor)
secondClassFields.foreach(s =>
c.typecheck(q"$s", silent = true) match {
case EmptyTree => println(s"can't typecheck $s while expanding @RemoveOptionFromFields for $className")
case t => println(s"field ${t.symbol} of type ${t.tpe}, subtype of Option: ${t.tpe <:< typeOf[Option[_]]}")
}
)
}
q"$mods val $name: $second = $rhs"
} else field
case _ =>
field
}
}
val withOptionRemovedFromFieldsClassDecl = q"case class WithOptionRemovedFromFields(..$fieldsWithoutOption)"
val newCompanionDecl = compDeclOpt.fold(
q"""
object ${className.toTermName} {
$withOptionRemovedFromFieldsClassDecl
}
"""
) {
compDecl =>
val q"$mods object $obj extends { ..$earlydefns } with ..$bases { $self => ..$body }" = compDecl
q"""
$mods object $obj extends { ..$earlydefns } with ..$bases { $self =>
..$body
$withOptionRemovedFromFieldsClassDecl
}
"""
}
q"""
$classDecl
$newCompanionDecl
"""
}
}
annottees match {
case (classDecl: ClassDef) :: Nil => modifiedClass(classDecl, None)
case (classDecl: ClassDef) :: (compDecl: ModuleDef) :: Nil => modifiedClass(classDecl, Some(compDecl))
case _ => c.abort(c.enclosingPosition, "This annotation only supports classes")
}
}
}
Beantwortet von – Dmytro Mitin
Antwort geprüft von – Jay B. (FixError Admin)