Frage

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

War es hilfreich?

Lösung

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.

Andere Tipps

This is the solution of Travis where all parts are put together:

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

  }

}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top