234

Let's say I have a case class that represents personas, people on different social networks. Instances of that class are fully immutable, and are held in immutable collections, to be eventually modified by an Akka actor.

Now, I have a case class with many fields, and I receive a message that says I must update one of the fields, something like this:

case class Persona(serviceName  : String,
                   serviceId    : String,
                   sentMessages : Set[String])

// Somewhere deep in an actor
val newPersona = Persona(existingPersona.serviceName,
                         existingPersona.serviceId,
                         existingPersona.sentMessages + newMessage)

Notice I have to specify all fields, even though only one changes. Is there a way to clone existingPersona and replace only one field, without specifying all the fields that don't change? Can I write that as a trait and use it for all my case classes?

If Persona was a Map-like instance, it would be easy to do.

François Beausoleil
  • 16,265
  • 11
  • 67
  • 90

5 Answers5

350

case classcomes with a copy method that is dedicated exactly to this usage:

val newPersona = existingPersona.copy(sentMessages = 
                   existingPersona.sentMessages + newMessage)
Nicolas
  • 24,509
  • 5
  • 60
  • 66
  • 5
    Where is that documented? I can't find a reference to copy in the "obvious" spots, http://www.scala-lang.org/api/current/index.html for instance. – François Beausoleil Aug 30 '11 at 20:38
  • 7
    It's a features of the language, you can find it in the Scala specification: http://www.scala-lang.org/docu/files/ScalaReference.pdf §5.3.2. It's not in the API because it's not a part of the API ;) – Nicolas Aug 30 '11 at 20:44
  • @François Beausoleil: I will file a bug and fix it. – soc Aug 31 '11 at 09:19
  • @soc As it is note relative to a given class how do you plan to fix it? – Nicolas Aug 31 '11 at 09:52
  • I will have to look into it. Imho all classes should show all available methods with their default values. – soc Aug 31 '11 at 09:56
  • Oh, ok, but this is another issue. Here the problem was to know that `case` keyword creates such a method. – Nicolas Aug 31 '11 at 10:05
  • 1
    I intended to make ScalaDoc show the copy methods when they exist, isn't that what you want? – soc Aug 31 '11 at 10:08
  • 4
    It's would be nice. But here, the problem of François (if I'm right) is that he didn't know that he will have a `copy` method if he declares a `case class`. – Nicolas Aug 31 '11 at 10:43
  • 1
    @Nicolas Yes, but if the copy method was documented in ScalaDoc, directly on Object, then I may have seen it. Although, not *all* objects sport that method. I see what you mean now. Tricky. – François Beausoleil Sep 01 '11 at 12:36
  • 2
    @JonathanNeufeld You will make many unfriends in the pure fp camp with that sentiment. I tend to agree with you. – WestCoastProjects Oct 29 '17 at 17:50
  • @JonathanNeufeld What's the point in criticizing Scala here? By the way, it's not an oddity, it's completely central to FP. And no, it does not automatically solve problems with parallelism (not relevant), no one says it does, it simply gives you other problems that some people think are easier to solve. – Richard Løvehjerte Nov 20 '19 at 13:06
51

Since 2.8, Scala case classes have a copy method that takes advantage of named/default params to work its magic:

val newPersona =
  existingPersona.copy(sentMessages = existing.sentMessages + newMessage)

You can also create a method on Persona to simplify usage:

case class Persona(
  svcName  : String,
  svcId    : String,
  sentMsgs : Set[String]
) {
  def plusMsg(msg: String) = this.copy(sentMsgs = this.sentMsgs + msg)
}

then

val newPersona = existingPersona plusMsg newMsg
Kevin Wright
  • 49,540
  • 9
  • 105
  • 155
11
existingPersona.copy(sentMessages = existingPersona.sentMessages + newMessage)
Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234
2

Consider using lens in Shapeless library:

import shapeless.lens

case class Persona(serviceName  : String,
                   serviceId    : String,
                   sentMessages : Set[String])
// define the lens
val messageLens = lens[Persona] >> 'sentMessages 

val existingPersona = Persona("store", "apple", Set("iPhone"))

// When you need the new copy, by setting the value,
val newPersona1 = messageLens.set(existingPersona)(Set.empty)
// or by other operation based on current value.
val newPersona2 = messageLens.modify(existingPersona)(_ + "iPad")

// Results:
// newPersona1: Persona(store,apple,Set())
// newPersona2: Persona(store,apple,Set(iPhone, iPad))

Moreover, in case you have nested case classes, the getter and setter methods can be a bit tedious to compose. It will be a good chance to simplify by using lens library.

Please also refer to:

Kaihua
  • 336
  • 2
  • 10
0

I didn't want to include a big library to do complex lenses that let you set values deep in nested case classes. It turns out it is just a few lines of code in the scalaz library:

  /** http://stackoverflow.com/a/5597750/329496 */
  case class Lens[A, B](get: A => B, set: (A, B) => A) extends ((A) => B) with Immutable {
    def apply(whole: A): B = get(whole)

    def mod(a: A, f: B => B) = set(a, f(this (a)))

    def compose[C](that: Lens[C, A]) = Lens[C, B](
      c => this(that(c)),
      (c, b) => that.mod(c, set(_, b))
    )

    def andThen[C](that: Lens[B, C]) = that compose this
  }

You can then create lenses that set deeply nested values far easier than using the built in copy feature. Here is a link to a big set if complex lenses that that my library uses to set heavily nested values.

simbo1905
  • 6,321
  • 5
  • 58
  • 86