如何使用Scala宏在方法调用中建模命名参数?

如何使用Scala宏在方法调用中建模命名参数?,scala,scala-macros,Scala,Scala Macros,在一些用例中,创建一个对象的副本非常有用,该对象是一组案例类的案例类的实例,这些案例类具有共同的特定值 例如,让我们考虑下面的case类: 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

在一些用例中,创建一个对象的副本非常有用,该对象是一组案例类的案例类的实例,这些案例类具有共同的特定值

例如,让我们考虑下面的case类:

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)
如前所述,没有简单的方法可以这样抽象:

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)
Apply
(参见上面代码块的底部)的最后一个参数是参数列表(这里是“复制”方法的参数)。在新宏API的帮助下,如何将类型为
c.Expr[Option[Int]]
的给定
id
作为命名参数传递给copy方法

特别是下面的宏表达式

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?*/

表示。下面是一个更通用的实现:

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
的成员的case类上工作,无论其类型是什么:

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)
如果case类没有
id
成员,
withId
将编译它,但它不会做任何事情。如果在这种情况下需要编译错误,可以在
copy
上为匹配添加额外条件


编辑:正如Eugene Burmako刚刚指出的,您可以在结尾处使用
AssignOrNamedArg
更自然地编写这篇文章:

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

如果case类没有
id
成员,则不会编译此版本,但无论如何,这更可能是所需的行为。

这是Travis的解决方案,其中所有部分都放在一起:

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

  }

}

谢谢,我喜欢这个解决方案的简洁性。它非常适用于我的用例。可能s.paramss.head部分需要额外检查空方法(=没有参数列表的方法),即s.paramss返回list()/Nil时。但结果是一样的:宏无法应用。@DanielDietrich:这一点很好,我已经添加了该检查,但请注意,这只是一个草图,修订版中至少有一个类似的假设(只有一个方法名为
copy
)。幸运的是,可能发生的最糟糕的情况是编译时错误有点混乱。是的,你是对的。正如您在第一篇帖子中所说,可以检查param id是否存在。在当前解决方案中,如果缺少参数id,则已经存在编译错误。为了获得更详细的编译器错误消息,我将模式匹配的if保护更改为(s.paramss.flatte.map(u.name).contains(newTermName(“id”))。这样,也可以捕获空值方法。params值不再使用,因此最终的c.Expr可以由模式匹配的第一个case部分返回:),这非常有用!但是有没有一种方法可以将id
暴露给多个case类共享的trait呢?在这里的所有示例中,我们已经有一个case类的实例,所以我想
copy
也可以被使用?
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!")
    }

  }

}