23

共通の特定の値を持つ一連のケース クラスのケース クラスのインスタンスであるオブジェクトのコピーを作成すると便利なユース ケースがあります。

たとえば、次のケース クラスを考えてみましょう。

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' のパラメーター)。id新しいマクロ API を使用して、指定された型c.Expr[Option[Int]]を名前付きパラメーターとして 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?*/ます。

4

2 に答える 2

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

  }

}
于 2012-11-19T20:21:35.547 に答える