如何使用 Scala 宏对方法调用中的命名参数建模? [英] Howto model named parameters in method invocations with Scala macros?

查看:36
本文介绍了如何使用 Scala 宏对方法调用中的命名参数建模?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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 }//这*不适用于案例类def withId[T <: Copyable[T]](obj: T, newId: Option[Int]): T =obj.copy(id = newId)

所以我创建了一个 Scala 宏,它(几乎)完成了这项工作:

import scala.reflect.macros.Context对象实体{导入 scala.language.experimental.macros导入 scala.reflect.macros.Contextdef 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] = {导入 c.universe._val currentType = entity.actualType//反射助手def equals(that: Name, name: String) = that.encoded == name ||that.decoded == 名称def hasName(name: String)(隐式方法: MethodSymbol) = equals(method.name, name)def hasReturnType(`type`: Type)(隐式方法: MethodSymbol) = method.typeSignature match {case MethodType(_, returnType) =>`type` == returnType}def hasParameter(name: String, `type`: Type)(隐式方法: MethodSymbol) = method.typeSignature match {case MethodType(params, _) =>params.exists { param =>等于(param.name, name) &&param.typeSignature == `type`}}//查找方法 entity.copy(id: Option[Int])currentType.members.find { 符号 =>symbol.isMethod &&{隐式 val 方法 = symbol.asMethodhasName("copy") &&hasReturnType(currentType) &&hasParameter("id", typeOf[Option[Int]])}} 比赛 {case Some(symbol) =>{val 方法 = 符号.asMethodval 参数 = reify((c.Expr[String](Literal(Constant("id"))).splice,id.splice)).treec.Expr(申请(选择(reify(entity.splice).tree,newTermName("copy")),列表(/*id.tree*/)))}情况无=>c.abort(c.enclosurePosition, currentType + " 需要方法 'copy(..., id: Option[Int], ...): " + currentType + "'")}}}

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

特别是下面的宏表达式

c.Expr(申请(选择(reify(entity.splice).tree,newTermName("copy")),列表(/*?id?*/)))

应该导致

entity.copy(id = id)

所以下面成立

case class Test(s: String, id: Option[Int] = None)//必须自己编译对象测试扩展应用{assert( Entity.withId(Test("scala rulz"), Some(1)) == Test("scala rulz", Some(1)))}

缺失的部分由占位符/*?id?*/表示.

解决方案

这是一个更通用的实现:

import scala.language.experimental.macros对象 WithIdExample {导入 scala.reflect.macros.Contextdef withId[T, I](entity: T, id: I): T = macro withIdImpl[T, I]def withIdImpl[T: c.WeakTypeTag, I: c.WeakTypeTag](c: Context)(实体:c.Expr[T],id:c.Expr[I]): c.Expr[T] = {导入 c.universe._val 树 = reify(entity.splice).treeval copy = entity.actualType.member(newTermName("copy"))val 参数 = 复制匹配 {case s: MethodSymbol if (s.paramss.nonEmpty) =>参数头案例_ =>c.abort(c.enclosurePosition, "没有符合条件的复制方法!")}c.Expr[T](应用(选择(树,复制),参数.map {case p if p.name.decoded == "id" =>reify(id.splice).tree情况p=>选择(树,p.name)}))}}

它适用于具有名为 id 的成员的任何 case 类,无论其类型是什么:

scala>case class Bar(arg0: String, id: Option[Int])定义类 Bar标度>case class Foo(x: Double, y: String, id: Int)定义类 Foo标度>WithIdExample.withId(Bar("bar", None), Some(2))res0: Bar = Bar(bar,Some(2))标度>WithIdExample.withId(Foo(0.0, "foo", 1), 2)res1:Foo = Foo(0.0,foo,2)

如果 case 类没有 id 成员,withId 会编译——它不会做任何事情.如果您希望在这种情况下出现编译错误,您可以在 copy 上的匹配项中添加额外的条件.

<小时>

正如 Eugene Burmako 刚刚指出的在 Twitter 上,你可以多写一点自然地在末尾使用 AssignOrNamedArg:

c.Expr[T](应用(选择(树,复制),AssignOrNamedArg(Ident("id"), reify(id.splice).tree) :: Nil))

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

There are use cases where it is useful to create a copy of an object which is an instance of a case class of a set of case classes, which have a specific value in common.

For example let's consider the following case classes:

case class Foo(id: Option[Int])
case class Bar(arg0: String, id: Option[Int])
case class Baz(arg0: Int, id: Option[Int], arg2: String)

Then copy can be called on each of these case class instances:

val newId = Some(1)

Foo(None).copy(id = newId)
Bar("bar", None).copy(id = newId)
Baz(42, None, "baz").copy(id = newId)

As described here and here there is no simple way to abstract this like this:

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)

So I created a scala macro, which does this job (almost):

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 + "'")
    }

  }

}

The last argument of Apply (see bottom of above code block) is a List of parameters (here: parameters of method 'copy'). How can the given id of type c.Expr[Option[Int]] be passed as named parameter to the copy method with the help of the new macro API?

In particular the following macro expression

c.Expr(
  Apply(
    Select(
      reify(entity.splice).tree,
      newTermName("copy")),
    List(/*?id?*/)))

should result in

entity.copy(id = id)

so that the following holds

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)))

}

The missing part is denoted by the placeholder /*?id?*/.

解决方案

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.

这篇关于如何使用 Scala 宏对方法调用中的命名参数建模?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆