【问题标题】:Howto model named parameters in method invocations with Scala macros?如何使用 Scala 宏对方法调用中的命名参数进行建模?
【发布时间】:2012-11-19 01:27:17
【问题描述】:

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

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

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

特别是下面的宏表达式

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?*/表示。

【问题讨论】:

    标签: scala scala-macros


    【解决方案1】:

    这是一个更通用的实现:

    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 成员,则此版本将无法编译,但无论如何这更有可能是所需的行为。

    【讨论】:

    • 谢谢,我喜欢这个解决方案的简洁性。它适用于我的用例。也许 s.paramss.head 部分需要额外检查空方法(= 没有参数列表的方法),即当 s.paramss 返回 List() / Nil 时。但结果是一样的:无法应用宏。
    • @DanielDietrich:很好,我已经添加了该检查,但请注意,这只是一个草图,在修订版中仍然至少有一个类似的假设(只有一个名为 @ 的方法987654331@)。幸运的是,可能发生的最坏情况是一个有点令人困惑的编译时错误。
    • 是的,你是对的。正如您在第一篇文章中所说,可以检查参数 id 的存在。在当前解决方案中,如果缺少参数 id,则已经存在编译错误。为了获得更详细的编译器错误消息,我会将模式匹配的 if-guard 更改为 (s.paramss.flatten.map(_.name).contains(newTermName("id")))。这样,空方法也被捕获。
    • 顺便说一句。 params 值不再使用,因此最终的 c.Expr 可以由模式匹配的第一个 case 部分返回:)
    • 这很有帮助!但是有没有办法将withId 暴露给几个案例类共享的特征?在这里的所有示例中,我们已经有一个案例类的实例,所以我想也可以使用copy
    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 2011-06-25
      • 1970-01-01
      • 2018-06-26
      • 2018-01-30
      • 2012-10-15
      • 1970-01-01
      • 1970-01-01
      • 2020-05-01
      • 1970-01-01
      相关资源
      最近更新 更多