Comment cloner une instance de classe de cas et modifier un seul champ dans Scala?

208

Disons que j'ai une classe de cas qui représente des personnages, des gens sur différents réseaux sociaux. Les instances de cette classe sont entièrement immuables et sont conservées dans des collections immuables, pour être éventuellement modifiées par un acteur Akka.

Maintenant, j'ai une classe de cas avec de nombreux champs et je reçois un message qui me dit que je dois mettre à jour l'un des champs, quelque chose comme ceci:

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)

Remarquez que je dois spécifier tous les champs, même si un seul change. Existe-t-il un moyen de cloner la persone existante et de remplacer un seul champ, sans spécifier tous les champs qui ne changent pas? Puis-je écrire cela comme un trait et l'utiliser pour toutes mes classes de cas?

Si Persona était une instance de type carte, ce serait facile à faire.

François Beausoleil
la source

Réponses:

324

case classest livré avec une copyméthode dédiée à cet usage:

val newPersona = existingPersona.copy(sentMessages = 
                   existingPersona.sentMessages + newMessage)
Nicolas
la source
5
Où est-ce documenté? Je ne trouve pas de référence à copier dans les endroits "évidents", scala-lang.org/api/current/index.html par exemple.
François Beausoleil
6
C'est une des caractéristiques du langage, vous pouvez le trouver dans la spécification Scala: scala-lang.org/docu/files/ScalaReference.pdf §5.3.2. Ce n'est pas dans l'API car il ne fait pas partie de l'API;)
Nicolas
1
J'avais l'intention de faire en sorte que ScalaDoc affiche les méthodes de copie lorsqu'elles existent, n'est-ce pas ce que vous voulez?
soc
4
Ce serait bien. Mais ici, le problème de François (si j'ai raison) est qu'il ne savait pas qu'il aura une copyméthode s'il déclare a case class.
Nicolas
2
@JonathanNeufeld Vous ferez de nombreux amis dans le camp fp pur avec ce sentiment. J'ai tendance à être d'accord avec toi.
javadba
46

Depuis la version 2.8, les classes de cas Scala ont une copyméthode qui tire parti des paramètres nommés / par défaut pour opérer sa magie:

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

Vous pouvez également créer une méthode Personapour simplifier l'utilisation:

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

puis

val newPersona = existingPersona plusMsg newMsg
Kevin Wright
la source
10
existingPersona.copy(sentMessages = existingPersona.sentMessages + newMessage)
Jean-Philippe Pellet
la source
0

Pensez à utiliser lensdans la Shapelessbibliothèque:

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))

De plus, si vous avez des classes de cas imbriquées , les méthodes getteret setterpeuvent être un peu fastidieuses à composer. Ce sera une bonne occasion de simplifier en utilisant la bibliothèque de lentilles.

Veuillez également vous référer à:

Kaihua
la source
0

Je ne voulais pas inclure une grande bibliothèque pour faire des lentilles complexes qui vous permettent de définir des valeurs profondes dans les classes de cas imbriquées. Il s'avère que ce ne sont que quelques lignes de code dans la bibliothèque scalaz:

  /** 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
  }

Vous pouvez ensuite créer des objectifs qui définissent des valeurs profondément imbriquées beaucoup plus facilement que d'utiliser la fonction de copie intégrée. Voici un lien vers un grand ensemble d'objectifs complexes que ma bibliothèque utilise pour définir des valeurs fortement imbriquées.

simbo1905
la source