Como clonar uma instância de classe de caso e alterar apenas um campo no Scala?

208

Digamos que eu tenha uma classe de caso que represente personas, pessoas em diferentes redes sociais. Instâncias dessa classe são totalmente imutáveis, e são mantidas em coleções imutáveis, para serem eventualmente modificadas por um ator Akka.

Agora, tenho uma classe de caso com muitos campos e recebo uma mensagem dizendo que devo atualizar um dos campos, algo como isto:

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)

Observe que eu tenho que especificar todos os campos, mesmo que apenas um seja alterado. Existe uma maneira de clonar o existentePersona e substituir apenas um campo, sem especificar todos os campos que não mudam? Posso escrever isso como uma característica e usá-lo para todas as minhas classes de casos?

Se o Persona fosse uma instância do tipo mapa, seria fácil.

François Beausoleil
fonte

Respostas:

324

case classvem com um copymétodo dedicado exatamente a esse uso:

val newPersona = existingPersona.copy(sentMessages = 
                   existingPersona.sentMessages + newMessage)
Nicolas
fonte
5
Onde isso está documentado? Não consigo encontrar uma referência para copiar nos pontos "óbvios", scala-lang.org/api/current/index.html , por exemplo.
François Beausoleil
6
É um recurso do idioma, você pode encontrá-lo na especificação Scala: scala-lang.org/docu/files/ScalaReference.pdf §5.3.2. Não está na API porque não faz parte da API.)
Nicolas
1
Eu pretendia fazer o ScalaDoc mostrar os métodos de cópia quando eles existem, não é isso que você deseja?
soc
4
Seria bom. Mas aqui, o problema de François (se eu estiver certo) é que ele não sabia que teria um copymétodo se declarasse a case class.
Nicolas
2
@ JonathanNeufeld Você fará muitos amigos no campo dos FPs com esse sentimento. Eu tendem a concordar com você.
Javadba
46

Desde a 2.8, as classes de caso Scala têm um copymétodo que tira proveito dos parâmetros nomeados / padrão para trabalhar sua mágica:

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

Você também pode criar um método Personapara simplificar o uso:

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

então

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

Considere usar lensna Shapelessbiblioteca:

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

Além disso, no caso de você aninhar classes de caso, os métodos gettere setterpodem ser um pouco entediantes de compor. Será uma boa chance de simplificar usando a biblioteca de lentes.

Consulte também:

Kaihua
fonte
0

Eu não queria incluir uma grande biblioteca para fazer lentes complexas que permitam definir valores profundos em classes de casos aninhadas. Acontece que são apenas algumas linhas de código na biblioteca 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
  }

Você pode criar lentes que definem valores profundamente aninhados muito mais facilmente do que usar o recurso de cópia incorporado. Aqui está um link para um grande conjunto de lentes complexas que minha biblioteca usa para definir valores fortemente aninhados.

simbo1905
fonte