scala - 如何使用 Scala 宏对方法调用中的命名参数进行建模?

标签 scala scala-macros

在某些用例中,创建一个对象的副本很有用,该对象是一组案例类的一个案例类的实例,它们具有共同的特定值。

例如,让我们考虑以下案例类:

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)

如所述herehere没有像这样抽象的简单方法:
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'的参数)。怎么给定id类型 c.Expr[Option[Int]]在新宏 API 的帮助下作为命名参数传递给复制方法?

特别是以下宏表达式
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 的成员的案例类。 ,无论它的类型是什么:
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成员(member), withId将编译——它只是不会做任何事情。如果你想在这种情况下出现编译错误,你可以在 copy 上的匹配中添加一个额外的条件。 .

编辑:正如 Eugene Burmako 刚刚指出的 on Twitter , 你可以用 AssignOrNamedArg 写得更自然一些在末尾:
c.Expr[T](Apply(
  Select(tree, copy),
  AssignOrNamedArg(Ident("id"), reify(id.splice).tree) :: Nil
))

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

关于scala - 如何使用 Scala 宏对方法调用中的命名参数进行建模?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13446528/

相关文章:

scala - 如何使用宏注释将无参数构造函数添加到 Scala 案例类?

内部类中的 Scala 原生方法

scala - 如何在非对称系统中将对象发送到远程 akka actor

scala - 不变类实例列表的 LUB

scala - Scala 3 宏中的显式类型转换

scala - 如何将 `andThen` Akka `Receive` s?

java - Scala:在 TreeView 中显示 systemDrive

scala - 使用 quasiquotes 从头生成同伴类的正确方法是什么?

scala - 编译器将结构类型用于在白盒宏内生成的案例类