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?*/
.
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. –
@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. –
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. –