2012-11-19 19 views
23

Esistono casi d'uso in cui è utile creare una copia di un oggetto che è un'istanza di una classe case di un insieme di classi case, che hanno un valore specifico in comune.Come modellare i parametri denominati nelle chiamate al metodo con macro Scala?

Per esempio prendiamo in considerazione le seguenti classi case:

case class Foo(id: Option[Int]) 
case class Bar(arg0: String, id: Option[Int]) 
case class Baz(arg0: Int, id: Option[Int], arg2: String) 

Poi copy può essere chiamato su ciascuno di questi casi caso di classe:

val newId = Some(1) 

Foo(None).copy(id = newId) 
Bar("bar", None).copy(id = newId) 
Baz(42, None, "baz").copy(id = newId) 

come descritto here e here non esiste un modo semplice come questo:

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) 

così ho creato una macro scala, che fa questo lavoro (quasi):

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 + "'") 
    } 

    } 

} 

L'ultimo argomento di Apply (vedi fondo blocco di codice sopra) è un elenco di parametri (qui: parametri del metodo di 'copia'). Come può il dato c.Expr[Option[Int]] del tipo essere passato come parametro con nome al metodo di copia con l'aiuto della nuova API macro?

In particolare, la seguente macro espressione

c.Expr(
    Apply(
    Select(
     reify(entity.splice).tree, 
     newTermName("copy")), 
    List(/*?id?*/))) 

dovrebbe comportare

entity.copy(id = id) 

cosicché la seguente contiene

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))) 

} 

La parte mancante è indicato con il segnaposto /*?id?*/.

risposta

20

Ecco un'implementazione che è anche un po 'più generico:

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) 
     } 
    )) 
    } 
} 

che funzionerà su qualsiasi classe caso con un membro denominato id, non importa quale sia il suo tipo è:

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) 

Se la classe del caso non ha un membro id, withId verrà compilato-non farà nulla. Se si desidera un errore di compilazione in tal caso, è possibile aggiungere una condizione aggiuntiva alla partita su copy.


Edit: Come Eugene Burmako appena sottolineato on Twitter, è possibile scrivere questo un po 'più naturalmente utilizzando AssignOrNamedArg alla fine:

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

Questa versione non verrà compilato se la classe caso doesn' t avere un membro id, ma è più probabile che sia comunque il comportamento desiderato.

+0

Grazie, mi piace la concisione di questa soluzione.Si applica bene al mio caso d'uso. Forse la parte s.paramss.head ha bisogno di un controllo extra per i metodi null (= metodi senza elenco di argomenti), cioè quando s.paramss restituisce List()/Nil. Ma il risultato è lo stesso: la macro non può essere applicata. –

+0

@DanielDietrich: buon punto, e ho aggiunto questo controllo, ma si noti che questo è solo uno schizzo e c'è ancora almeno un'ipotesi simile nella versione rivista (che esiste un solo metodo chiamato 'copia'). Fortunatamente il peggio che potrebbe accadere è un errore di compilazione in qualche modo confuso. –

+0

Sì, hai ragione. Come hai detto nel tuo primo post, potrebbe essere fatto un controllo sull'esistenza dell'ID param. Nella soluzione corrente c'è già un errore di compilazione, se manca l'ID param. Per ottenere un messaggio di errore del compilatore più dettagliato, cambierei l'if-guard della corrispondenza del modello con (s.paramss.flatten.map (_. Name) .contains (newTermName ("id"))). Con questo, vengono catturati anche i metodi null. –

2

Questa è la soluzione di Travis in cui tutte le parti sono messe insieme:

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!") 
    } 

    } 

} 
Problemi correlati