2012-11-19 6 views
23

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.

Antwort

20

Hier ist eine Implementierung, die auch ein wenig mehr generic ist: Es wird funktionieren auf jeden Fall Klasse mit einem Mitglied namens id, egal, was seine Art ist

import scala.language.experimental.macros 

object WithIdExample { 
    import scala.reflect.macros.Context 

    def withId[T, I](entity: T, id: I): T = macro withIdImpl[T, I] 

    def withIdImpl[T: c.WeakTypeTag, I: c.WeakTypeTag](c: Context)(
    entity: c.Expr[T], id: c.Expr[I] 
): c.Expr[T] = { 
    import c.universe._ 

    val tree = reify(entity.splice).tree 
    val copy = entity.actualType.member(newTermName("copy")) 

    val params = copy match { 
     case s: MethodSymbol if (s.paramss.nonEmpty) => s.paramss.head 
     case _ => c.abort(c.enclosingPosition, "No eligible copy method!") 
    } 

    c.Expr[T](Apply(
     Select(tree, copy), 
     params.map { 
     case p if p.name.decoded == "id" => reify(id.splice).tree 
     case p => Select(tree, p.name) 
     } 
    )) 
    } 
} 

:

scala> case class Bar(arg0: String, id: Option[Int]) 
defined class Bar 

scala> case class Foo(x: Double, y: String, id: Int) 
defined class Foo 

scala> WithIdExample.withId(Bar("bar", None), Some(2)) 
res0: Bar = Bar(bar,Some(2)) 

scala> WithIdExample.withId(Foo(0.0, "foo", 1), 2) 
res1: Foo = Foo(0.0,foo,2) 

Wenn die Fallklasse hat kein id Mitglied, withId wird kompilieren-es wird einfach nichts tun. Wenn Sie in diesem Fall einen Kompilierungsfehler haben möchten, können Sie der Übereinstimmung unter copy eine zusätzliche Bedingung hinzufügen.


Edit: Wie Eugene Burmako nur on Twitter wies darauf hin, können Sie schreiben dies ein wenig mehr natürlich AssignOrNamedArg am Ende mit:

c.Expr[T](Apply(
    Select(tree, copy), 
    AssignOrNamedArg(Ident("id"), reify(id.splice).tree) :: Nil 
)) 

Diese Version, wenn der Fall Klasse doesn‘wird nicht kompiliert t haben ein id Mitglied, aber das ist wahrscheinlich das gewünschte Verhalten sowieso.

+0

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. –

+0

@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. –

+0

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. –

2

Dies ist die Lösung von Travis, wo alle Teile zusammengesetzt werden:

import scala.language.experimental.macros 

object WithIdExample { 

    import scala.reflect.macros.Context 

    def withId[T, I](entity: T, id: I): T = macro withIdImpl[T, I] 

    def withIdImpl[T: c.WeakTypeTag, I: c.WeakTypeTag](c: Context)(
    entity: c.Expr[T], id: c.Expr[I] 
): c.Expr[T] = { 

    import c.universe._ 

    val tree = reify(entity.splice).tree 
    val copy = entity.actualType.member(newTermName("copy")) 

    copy match { 
     case s: MethodSymbol if (s.paramss.flatten.map(_.name).contains(
     newTermName("id") 
    )) => c.Expr[T](
     Apply(
      Select(tree, copy), 
      AssignOrNamedArg(Ident("id"), reify(id.splice).tree) :: Nil)) 
     case _ => c.abort(c.enclosingPosition, "No eligible copy method!") 
    } 

    } 

} 
Verwandte Themen