3

If you have a case class like:

case class Foo(x: String, y: String, z: String)

And you have two instances like:

Foo("x1","y1","z1")
Foo("x2","y2","z2")

Is it possible to merge instance 1 in instance 2, except for field z, so that the result would be:

Foo("x1","y1","z2")

My usecase is just that I give JSON objects to a Backbone app through a Scala API, and the Backbone app gives me back a JSON of the same structure so that I can save/update it. These JSON objects are parsed as case class for easy Scala manipulation. But some fields should never be updated by the client side (like creationDate). For now I'm doing a manual merge but I'd like a more generic solution, a bit like an enhanced copy function.

What I'd like is something like this:

instanceFromDB.updateWith(instanceFromBackbone, excludeFields = "creationDate" )

But I'd like it to be typesafe :)

Edit: My case class have a lot more fields and I'd like the default bevavior to merge fields unless I explicitly say to not merge them.

Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
  • 2
    I don't understand why you don't just do the merge in reverse: you want to keep field `z`, so you copy it from instance 1 to instance 2. Then you have exactly everything in instance 2 by default, except for what you explicitly copy over from 1. (It's all immutable so the new thing will be neither 1 nor 2 anyway; it doesn't matter who you start with.) The only case where this wouldn't work is where the two instances _aren't actually the same case class_. – Rex Kerr Jan 12 '13 at 01:19
  • that would be an accepted answer :) I just didnd't though about it. I do not need polymorphism right now. – Sebastien Lorber Jan 12 '13 at 01:28
  • Considering your use-case `instanceFromDB.updateWith` It's not robust to exclude some fields, you had some long list of values to update, but now ended up with kind of opposite logic (small list of values you don't update). When you add new fields you also must check that small list, otherwise client will overwrite them, and that's a serious error. If a client does not update some field - rather marginal bug. – idonnie Jan 12 '13 at 03:58

4 Answers4

4

What you want is already there; you just need to approach the problem the other way.

case class Bar(x: String, y: String)
val b1 = Bar("old", "tired")
val b2 = Bar("new", "fresh")

If you want everything in b2 not specifically mentioned, you should copy from b2; anything from b1 you want to keep you can mention explicitly:

def keepY(b1: Bar, b2: Bar) = b2.copy(y = b1.y)

scala> keepY(b1, b2)
res1: Bar = Bar(new,tired)

As long as you are copying between two instances of the same case class, and the fields are immutable like they are by default, this will do what you want.

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
3
case class Foo(x: String, y: String, z: String)

Foo("old_x", "old_y", "old_z")
// res0: Foo = Foo(old_x,old_y,old_z)

Foo("new_x", "new_y", "new_z")
// res1: Foo = Foo(new_x,new_y,new_z)

// use copy() ...
res0.copy(res1.x, res1.y)
// res2: Foo = Foo(new_x,new_y,old_z)

// ... with by-name parameters
res0.copy(y = res1.y)
// res3: Foo = Foo(old_x,new_y,old_z)
idonnie
  • 1,703
  • 12
  • 11
  • That would work fine but I'd like to not have to matter when a new attribute is added to the case class. I would like it to be merged by default unless I explicitly say it should not. – Sebastien Lorber Jan 12 '13 at 00:54
  • Implement `def copy(f: Foo, butSkip: Symbol*)` – idonnie Jan 12 '13 at 01:02
  • Can you provide a code please? Your function seems like I have to update it everytime I add a new attribute that should be merge right? – Sebastien Lorber Jan 12 '13 at 01:22
  • Its not that easy to write it robust enough, you may be interested in this discussion: http://stackoverflow.com/questions/2224251/reflection-on-a-scala-case-class – idonnie Jan 12 '13 at 03:27
3

You can exclude class params from automatic copying by the copy method by currying:

case class Person(name: String, age: Int)(val create: Long, val id: Int)

This makes it clear which are ordinary value fields which the client sets and which are special fields. You can't accidentally forget to supply a special field.

For the use case of taking the value fields from one instance and the special fields from another, by reflectively invoking copy with either default args or the special members of the original:

import scala.reflect._
import scala.reflect.runtime.{ currentMirror => cm }
import scala.reflect.runtime.universe._
import System.{ currentTimeMillis => now }

case class Person(name: String, age: Int = 18)(val create: Long = now, val id: Int = Person.nextId) {
  require(name != null)
  require(age >= 18)
}
object Person {
  private val ns = new java.util.concurrent.atomic.AtomicInteger
  def nextId = ns.getAndIncrement()
}

object Test extends App {

  /** Copy of value with non-defaulting args from model. */
  implicit class Copier[A: ClassTag : TypeTag](val value: A) {
    def copyFrom(model: A): A = {
      val valueMirror = cm reflect value
      val modelMirror = cm reflect model
      val name = "copy"
      val copy = (typeOf[A] member TermName(name)).asMethod

      // either defarg or default val for type of p
      def valueFor(p: Symbol, i: Int): Any = {
        val defarg = typeOf[A] member TermName(s"$name$$default$$${i+1}")
        if (defarg != NoSymbol) {
          println(s"default $defarg")
          (valueMirror reflectMethod defarg.asMethod)()
        } else {
          println(s"def val for $p")
          val pmethod = typeOf[A] member p.name
          if (pmethod != NoSymbol) (modelMirror reflectMethod pmethod.asMethod)()
          else throw new RuntimeException("No $p on model")
        }
      }
      val args = (for (ps <- copy.paramss; p <- ps) yield p).zipWithIndex map (p => valueFor(p._1,p._2))
      (valueMirror reflectMethod copy)(args: _*).asInstanceOf[A]
    }
  }
  val customer  = Person("Bob")()
  val updated   = Person("Bobby", 37)(id = -1)
  val merged    = updated.copyFrom(customer)
  assert(merged.create == customer.create)
  assert(merged.id == customer.id)
}
som-snytt
  • 39,429
  • 2
  • 47
  • 129
1
case class Foo(x: String, y: String, z: String)

val foo1 = Foo("x1", "y1", "z1")
val foo2 = Foo("x2", "y2", "z2")

val mergedFoo = foo1.copy(z = foo2.z) // Foo("x1", "y1", "z2")

If you change Foo later to:

case class Foo(w: String, x: String, y: String, z: String)

No modification will have to be done. Explicitly:

val foo1 = Foo("w1", "x1", "y1", "z1")
val foo2 = Foo("w2", "x2", "y2", "z2")

val mergedFoo = foo1.copy(z = foo2.z) // Foo("w1", "x1", "y1", "z2")
Alex DiCarlo
  • 4,851
  • 18
  • 34