¿Puedo actualizar un object inmutable profundamente nested sin tomar conciencia de su context?

Imaginemos que tengo un gráfico de objects inmutables nested, a lo largo de estas líneas (utilizando la syntax de Kotlin, pero espero que esté claro):

data class House(val bedroom: Bedroom, val bathroom: Bathroom, val kitchen: Kitchen) class Bedroom class Bathroom data class Kitchen(val oven: Oven, val kettle: Kettle) class Oven data class Kettle(val on: Boolean) var house = House(Bedroom(), Bathroom(), Kitchen(Oven(), Kettle(false))) 

Ahora, quiero encender el hervidor. Si los objects fueran mutables, simplemente escribiría:

 data class Kettle(var on: Boolean) { fun activate() { this.on = true } } house.kitchen.kettle.activate() 

Pero como son inmutables, tengo que escribir:

 data class Kettle(val on: Boolean) { fun activate(house: House): House { return house.copy(kitchen = kitchen.copy(kettle = kettle.copy(on = true))) } } house = house.kitchen.kettle.activate(house) 

(En realidad, es un poco más complicado, pero este pseudo-código lo hará).

No me gusta esto, no porque sea largo, per se, sino porque Kettle ahora necesita saber no solo sobre su propio estado interno, sino sobre el context completo en el que existe.

¿Cómo puedo reescribir esto para que cada object pueda ser responsable de proporcionar su propia lógica de mutación, sin tener que estar al tanto del gráfico completo del object? ¿O simplemente estoy intentando casarme con conceptos orientados a objects y funcionales de una manera imposible?

Un posible enfoque en el que pensé (esta es la pregunta de asker, por cierto) fue así:

 data class Kettle(val on: Boolean) { fun activate() { return Transform(this, Kettle(on = true)) } } class Transform<T>(val what: T, val replacement: T) { fun <U> apply(x: U): U { if (x is T && x == what) { return replacement as U } else { return x } } } 

La idea aquí es que una transformación es una function que puede aplicar a cada object en el gráfico y solo modificará what que le indicó que modifique.

Entonces lo usa así:

 val transform = house.kitchen.kettle.activate() house = house.transformEverything(transform) 

Donde cada class tiene una implementación como esta:

 data class House(val bedroom: Bedroom, val bathroom: Bathroom, val kitchen: Kitchen) { fun transformEverything(transform: Transform): House { return transform(this).copy( bedroom = bedroom.transformEverything(transform), bathroom = bathroom.transformEverything(transform), kitchen = kitchen.transformEverything(transform) ) } } 

Que recursivamente le da a la transform oportunidad de modificar cada object que quiere, y solo lo aplicará a uno.

Este enfoque es malo porque:

  1. Toneladas de placa de caldera dando a todo su propia versión tonta del método transformEverything
  2. Parece extraño (e ineficiente) tener que llamar a una function en cada object en el gráfico solo para cambiar uno.

Pero logra mi objective de que Kettle no necesite saber nada sobre su context, y es bastante sencillo escribir la function de activate . ¿Pensamientos?

Aquí es donde las lentes funcionales muestran su poder. Por ejemplo, usando poetix / klenses ,

 val kettleLens = +House::kitchen + Kitchen::kettle var house = House(...) house = kettleLens(house) { copy(on = true) }