在某些用例中,创建对象的副本很有用,该对象是一组案例类的案例类的实例,它们具有特定的共同值。
例如,让我们考虑以下案例类:
case class Foo(id: Option[Int])
case class Bar(arg0: String, id: Option[Int])
case class Baz(arg0: Int, id: Option[Int], arg2: String)
然后可以在每个案例类实例上调用copy
:
val newId = Some(1)
Foo(None).copy(id = newId)
Bar("bar", None).copy(id = newId)
Baz(42, None, "baz").copy(id = newId)
正如here和here所述,没有简单的方法可以像这样抽象出来:
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)
所以我创建了一个scala宏,它完成了这项工作(差不多):
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 + "'")
}
}
}
Apply
的最后一个参数(参见上面代码块的底部)是一个参数列表(这里:方法'copy'的参数)。在新的宏API的帮助下,如何将id
类型的c.Expr[Option[Int]]
作为命名参数传递给复制方法?
特别是以下宏表达式
c.Expr(
Apply(
Select(
reify(entity.splice).tree,
newTermName("copy")),
List(/*?id?*/)))
应该导致
entity.copy(id = id)
以便以下内容
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)))
}
缺少的部分由占位符/*?id?*/
表示。
答案 0 :(得分:20)
这是一个更通用的实现:
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)
}
))
}
}
它适用于任何名为id
的成员的案例类,无论其类型是什么:
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)
如果案例类没有id
成员,withId
将编译 - 它就不会做任何事情。如果您希望在这种情况下出现编译错误,可以在copy
上为匹配添加额外条件。
编辑:正如Eugene Burmako刚刚指出on Twitter,您最后可以使用AssignOrNamedArg
更自然地写出来:
c.Expr[T](Apply(
Select(tree, copy),
AssignOrNamedArg(Ident("id"), reify(id.splice).tree) :: Nil
))
如果案例类没有id
成员,则此版本将无法编译,但无论如何,这更可能是所需的行为。
答案 1 :(得分:2)
这是特拉维斯的解决方案,所有部分都放在一起:
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!")
}
}
}