Here’s an implementation that’s also a little more generic:
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)
}
))
}
}
It’ll work on any case class with a member named id
, no matter what its type is:
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)
If the case class doesn’t have an id
member, withId
will compile—it just won’t do anything. If you want a compile error in that case, you can add an extra condition to the match on copy
.
Edit: As Eugene Burmako just pointed out on Twitter, you can write this a little more naturally using AssignOrNamedArg
at the end:
c.Expr[T](Apply(
Select(tree, copy),
AssignOrNamedArg(Ident("id"), reify(id.splice).tree) :: Nil
))
This version won’t compile if the case class doesn’t have an id
member, but that’s more likely to be the desired behavior anyway.