scala macros inference

macros scala: defer type inference



(1)

weakTypeOf combinado con un contexto vinculado a la clase de la instancia proporciona la inferencia de tipo "retardada" deseada.

trait Entity[E <: Entity[E]]{self:E=> def id: Int def withId(id: Int) = MacroCopy.withIdImpl[E] } case class User(id: Int, name: String) extends Entity[User] object MacroCopy { import scala.language.experimental.macros import scala.reflect.macros.blackbox.Context def withIdImpl[T <: Entity[T]: c.WeakTypeTag] // context bound on Entity (c: Context)(id: c.Expr[Int]): c.Expr[T] = { import c.universe._ val tree = reify( c.Expr[T](c.prefix.tree).splice ).tree val copy = weakTypeOf[T].member(TermName("copy")) // now lookup case class'' copy method val params = copy match { case s: MethodSymbol if (s.paramLists.nonEmpty) => s.paramLists.head case _ => c.abort(c.enclosingPosition, "No eligible copy method!") } c.Expr[T](Apply( Select(tree, copy), AssignOrNamedArg(Ident(TermName("id")), reify(id.splice).tree) :: Nil )) } }

Ahora podemos escribir foo.withId(2) en lugar del intento anterior, foo.withId(foo, 2) , maravillosamente conciso. Se podría estar preguntando por qué no solo hacer: foo.copy(id = 2) ? Para el caso concreto que funciona bien, pero cuando necesita aplicar esto en un nivel más abstracto, entonces no funciona en absoluto.

Lo siguiente tampoco funciona, parece que debemos trabajar con instancias concretas de clases de casos, tan cerca ;-( Por ejemplo, digamos que tiene un DAO y desea asegurarse de que todas las entidades actualizadas tengan un ID válido. La macro anterior le permite para hacer algo como:

def update[T <: Entity[T]](entity: T, id: Int)(implicit ss: Session): Either[String,Unit] = { either( byID(id).mutate(_.row = entity.withId(id)), i18n("not updated") ) }

Como la entidad es un rasgo y no una clase de caso, sin la macro no habría forma de simular en tiempo de entity.copy(id = id) una entity.copy(id = id) . Como solución, he redefinido el método de actualización DAO de la siguiente manera:

def update[T <: Entity[T]](fn: id => T, id: Int)(implicit ss: Session): Either[String,Unit] = { either( byID(id).mutate(_.row = fn(id)), i18n("not updated") ) }

Eso al menos obliga a uno a suministrar la función u.withId (_: Int) al método de actualización. Es mejor que tener entidades potencialmente no válidas en tiempo de ejecución, pero aún así no es tan elegante como realizar con Id justo antes de que importe (es decir, persistir en DB), evitando así el trabajo de mulo (repetitivo) de pasar una instancia concreta de función para actualizar, suspiro, debe haber una forma de llevarlo a cabo con macros.

En otras noticias, habiendo escrito mi primer macro, realmente me gusta el potencial aquí, cosas increíbles ;-)

Preámbulo: esto se basa en la solución basada en macro de @Travis Brown para copiar las propiedades de la clase de caso.

Dado:

trait Entity[E <: Entity[E]]{self:E=> def id: Int def withId(id: Int) = MacroCopy.withId(self,id) } case class User(id: Int, name: String) extends Entity[User]

y la implementación de Macro:

object MacroCopy { import scala.language.experimental.macros import scala.reflect.macros.blackbox.Context def withId[T](entity: T, id: Int): T = macro withIdImpl[T] def withIdImpl[T: c.WeakTypeTag] (c: Context)(entity: c.Expr[T], id: c.Expr[Int]): c.Expr[T] = { import c.universe._ val tree = reify(entity.splice).tree val copy = entity.actualType.member(TermName("copy")) val params = copy match { case s: MethodSymbol if (s.paramLists.nonEmpty) => s.paramLists.head case _ => c.abort(c.enclosingPosition, "No eligible copy method!") } c.Expr[T](Apply( Select(tree, copy), AssignOrNamedArg(Ident(TermName("id")), reify(id.splice).tree) :: Nil )) } }

¿Hay alguna manera de diferir la inferencia de tipos de tal manera que la macro funcione en un User y no en el tipo de auto de la entidad? Tal como está, el verificador de tipos no sabe nada sobre el método de copia de clases de casos del User , ya que todo lo que ve es un valor de tipo E

Me gustaría hacer:

val u = User(2,"foo") u.withId(3)

La mayoría de las soluciones alternativas que he visto implican definir withId como abstracto en el rasgo de Entidad y luego implementar el método en cada clase de caso, preferiría evitar eso si es posible.