Scala: coletando atualizações / mudanças de estado imutável
Atualmente, estou tentando aplicar um estilo de programação mais funcional a um projeto envolvendo desenvolvimento de GUI de baixo nível (baseado em LWJGL). Obviamente, em tal caso, é necessário transportar um monte de estado, que é mutável na versão atual. Meu objetivo é finalmente ter um estado completamente imutável, a fim de evitar mudanças de estado como efeito colateral. Eu estudei as lentes de scalaz e monads de estado por algum tempo, mas minha principal preocupação permanece: todas essas técnicas dependem de copy-on-write. Como meu estado tem um grande número de campos e também alguns campos de tamanho considerável, estou preocupado com o desempenho.
Para meu conhecimento, a abordagem mais comum para modificar objetos imutáveis é usar o método gerado.copy
método de umcase class
(isso também é o que as lentes fazem sob o capô). Minha primeira pergunta é como issocopy
método é realmente implementado? Eu realizei algumas experiências com uma classe como:
case class State(
innocentField: Int,
largeMap: Map[Int, Int],
largeArray: Array[Int]
)
Por benchmarking e também olhando para a saída de-Xprof
parece que está atualizandosomeState.copy(innocentField = 42)
realmente realiza uma cópia profunda e eu observo uma queda significativa de desempenho quando eu aumentar o tamanho delargeMap
elargeArray
. Eu estava de alguma forma esperando que a instância recém-construída compartilha as referências de objeto do estado original, uma vez que internamente a referência deve ser passada para o construtor. Posso de alguma forma forçar ou desabilitar esse comportamento de cópia profunda do padrãocopy
?
Enquanto refletia sobre o problema da cópia na gravação, fiquei me perguntando se há soluções mais gerais para esse problema em FP, que armazenam alterações de dados imutáveis de uma maneira incremental (no sentido de "coletar atualizações" ou "reunir" alterar"). Para minha surpresa, não consegui encontrar nada, então tentei o seguinte:
// example state with just two fields
trait State {
def getName: String
def getX: Int
def setName(updated: String): State = new CachedState(this) {
override def getName: String = updated
}
def setX(updated: Int): State = new CachedState(this) {
override def getX: Int = updated
}
// convenient modifiers
def modName(f: String => String) = setName(f(getName))
def modX(f: Int => Int) = setX(f(getX))
def build(): State = new BasicState(getName, getX)
}
// actual (full) implementation of State
class BasicState(
val getName: String,
val getX: Int
) extends State
// CachedState delegates all getters to another state
class CachedState(oldState: State) extends State {
def getName = oldState.getName
def getX = oldState.getX
}
Agora isso permite fazer algo assim:
var s: State = new BasicState("hello", 42)
// updating single fields does not copy
s = s.setName("world")
s = s.setX(0)
// after a certain number of "wrappings"
// we can extract (i.e. copy) a normal instance
val ns = s.setName("ok").setX(40).modX(_ + 2).build()
Minha pergunta agora é: o que você acha desse design? Isso é algum tipo de padrão de design FP que eu não conheço (além da semelhança com o padrão Builder)? Desde que eu não encontrei nada semelhante, eu estou querendo saber se há algum problema importante com esta abordagem? Ou existem maneiras mais padronizadas de resolver o gargalo da cópia na gravação sem desistir da imutabilidade?
Existe ainda a possibilidade de unificar as funções get / set / mod de alguma forma?
Editar:
Minha suposição de quecopy
realiza uma cópia profunda foi de fato errado.