scala scala-macros

¿Cómo modelar parámetros nombrados en invocaciones de métodos con macros de Scala?



scala-macros (2)

Hay casos de uso en los que es útil crear una copia de un objeto que es una instancia de una clase de caso de un conjunto de clases de casos, que tienen un valor específico en común.

Por ejemplo, consideremos las siguientes clases de casos:

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

Luego se puede llamar a copy en cada una de estas instancias de clase de caso:

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

Como se describe here y here no hay una forma simple de abstraer esto de esta manera:

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)

Así que creé una macro scala, que hace este trabajo (casi):

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

El último argumento de Apply (ver el bloque de código inferior) es una lista de parámetros (aquí: parámetros del método ''copy''). ¿Cómo se puede c.Expr[Option[Int]] id de tipo c.Expr[Option[Int]] como parámetro con nombre al método de copia con la ayuda de la nueva macro API?

En particular, la siguiente expresión macro

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

debería resultar en

entity.copy(id = id)

de modo que lo que sigue es

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 que falta se indica con el marcador de posición /*?id?*/ .


Aquí hay una implementación que también es un poco más genérica:

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

Funcionará en cualquier clase de caso con un miembro llamado id , sin importar su 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)

Si la clase de caso no tiene un miembro de id , withId compilará, simplemente no hará nada. Si desea un error de compilación en ese caso, puede agregar una condición adicional a la coincidencia en la copy .

Editar: Como acaba de señalar Eugene Burmako en Twitter , puedes escribir esto de forma más natural usando AssignOrNamedArg al final:

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

Esta versión no se compilará si la clase de caso no tiene un miembro de id , pero de todos modos es más probable que sea el comportamiento deseado.


Esta es la solución de Travis donde todas las partes se juntan:

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