Es gibt Anwendungsfälle, in denen es nützlich ist, eine Kopie eines Objekts zu erstellen, das eine Instanz einer Fallklasse einer Menge von Fallklassen ist, die einen bestimmten gemeinsamen Wert haben.Wie modellieren benannte Parameter in Methodenaufrufen mit Scala-Makros?
Zum Beispiel wollen wir die folgenden Fallklassen berücksichtigen:
val newId = Some(1)
Foo(None).copy(id = newId)
Bar("bar", None).copy(id = newId)
Baz(42, None, "baz").copy(id = newId)
Wie beschrieben here und here es keine einfache Art und Weise ist:
case class Foo(id: Option[Int])
case class Bar(arg0: String, id: Option[Int])
case class Baz(arg0: Int, id: Option[Int], arg2: String)
Dann copy
kann auf jedem dieser Fall Klasseninstanzen aufgerufen werden um dies wie folgt zu abstrahieren:
type Copyable[T] = { def copy(id: Option[Int]): T }
// THIS DOES *NOT* WORK FOR CASE CLASSES
def withId[T <: Copyable[T]](obj: T, newId: Option[Int]): T =
obj.copy(id = newId)
Also habe ich ein scala Makro, die diesen Job macht (fast):
import scala.reflect.macros.Context
object Entity {
import scala.language.experimental.macros
import scala.reflect.macros.Context
def withId[T](entity: T, id: Option[Int]): T = macro withIdImpl[T]
def withIdImpl[T: c.WeakTypeTag](c: Context)(entity: c.Expr[T], id: c.Expr[Option[Int]]): c.Expr[T] = {
import c.universe._
val currentType = entity.actualType
// reflection helpers
def equals(that: Name, name: String) = that.encoded == name || that.decoded == name
def hasName(name: String)(implicit method: MethodSymbol) = equals(method.name, name)
def hasReturnType(`type`: Type)(implicit method: MethodSymbol) = method.typeSignature match {
case MethodType(_, returnType) => `type` == returnType
}
def hasParameter(name: String, `type`: Type)(implicit method: MethodSymbol) = method.typeSignature match {
case MethodType(params, _) => params.exists { param =>
equals(param.name, name) && param.typeSignature == `type`
}
}
// finding method entity.copy(id: Option[Int])
currentType.members.find { symbol =>
symbol.isMethod && {
implicit val method = symbol.asMethod
hasName("copy") && hasReturnType(currentType) && hasParameter("id", typeOf[Option[Int]])
}
} match {
case Some(symbol) => {
val method = symbol.asMethod
val param = reify((
c.Expr[String](Literal(Constant("id"))).splice,
id.splice)).tree
c.Expr(
Apply(
Select(
reify(entity.splice).tree,
newTermName("copy")),
List(/*id.tree*/)))
}
case None => c.abort(c.enclosingPosition, currentType + " needs method 'copy(..., id: Option[Int], ...): " + currentType + "'")
}
}
}
Das letzte Argument von Apply
(unten von oben Codeblock sehen) ist eine Liste der Parameter (hier: Parameter der Methode ‚Kopie‘). Wie kann der angegebene id
vom Typ c.Expr[Option[Int]]
als benannter Parameter an die Kopiermethode mit Hilfe der neuen Makro-API übergeben werden?
insbesondere in den folgenden Makroausdruck
c.Expr(
Apply(
Select(
reify(entity.splice).tree,
newTermName("copy")),
List(/*?id?*/)))
sollte zur Folge
entity.copy(id = id)
so dass die folgende
case class Test(s: String, id: Option[Int] = None)
// has to be compiled by its own
object Test extends App {
assert(Entity.withId(Test("scala rulz"), Some(1)) == Test("scala rulz", Some(1)))
}
Der fehlende Teil /*?id?*/
durch den Platzhalter bezeichnet hält.
Danke, ich mag die Prägnanz dieser Lösung.Es trifft gut auf meinen Anwendungsfall zu. Vielleicht benötigt der Teil s.paramss.head eine zusätzliche Überprüfung auf Nullmethoden (= Methoden ohne Argumentliste), d. H. Wenn s.paramss List()/Nil zurückgibt. Das Ergebnis ist jedoch das gleiche: Das Makro kann nicht angewendet werden. –
@DanielDietrich: Guter Punkt, und ich habe diese Prüfung hinzugefügt, aber beachte, dass dies nur eine Skizze ist und dass es immer noch mindestens eine ähnliche Annahme in der überarbeiteten Version gibt (dass es nur eine Methode mit dem Namen 'copy' gibt). Glücklicherweise ist das schlimmste, was passieren könnte, ein etwas verwirrender Kompilierungsfehler. –
Ja, du hast Recht. Wie Sie in Ihrem ersten Post gesagt haben, könnte eine Überprüfung auf die Existenz von Param ID gemacht werden. In der aktuellen Lösung gibt es bereits einen Kompilierfehler, wenn die Parameter-ID fehlt. Um eine ausführlichere Compiler-Fehlermeldung zu erhalten, würde ich den if-guard der Musterübereinstimmung auf (s.paramss.flatten.map (_. Name) .contains (newTermName ("id")) ändern. Damit werden auch nullare Methoden erfasst. –