问题
Preamble: this is based on @Travis Brown's macro based solution to copying case class properties.
Given:
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]
and the Macro implementation:
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
))
}
}
is there a way to somehow defer type inference in such a way that the macro operates on a User
and not the Entity's self type? As it stands the type checker knows nothing about the User
's case class copy method since all it sees is a value of type E
.
I'd like to do:
val u = User(2,"foo")
u.withId(3)
Most of the alternative solutions I've seen entail defining withId as abstract in the Entity trait and then implementing the method in every case class, would prefer to avoid that if possible.
回答1:
weakTypeOf
combined with a context bound on instance's class provides desired "delayed" type inference.
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
))
}
}
We can now write foo.withId(2)
in place of previous attempt, foo.withId(foo, 2)
, wonderfully concise. Might be wondering why not just do: foo.copy(id = 2)
? For the concrete case that works fine, but when you need to apply this at a more abstract level, then it works not at all.
The following also does not work, seems we must work with concrete case classes instances, so close ;-( For example, let's say you have a DAO and you want to ensure that all updated entities have a valid id. The above macro allows you to do something like:
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") )
}
Since Entity is a trait and not a case class, without the macro there would be no compile-time way of simulating an entity.copy(id = id)
. As a workaround I have redefined DAO update method as follows:
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") )
}
That at least forces one to supply u.withId(_:Int) function to the update method. Better than having potentially invalid entities at runtime, but still not as elegant as performing withId right before it matters (i.e. persisting to DB), thereby avoiding the mule work (boilerplate) of passing concrete function instance to update, sigh, there must be a way to pull this off with macros.
In other news, having written my first macro, I am really liking the potential here, awesome stuff ;-)
来源:https://stackoverflow.com/questions/25752645/scala-macros-defer-type-inference