Idiomatic immutability when updating value on class of class

Apologies if the title is confusing; I’m not sure how best to explain this in the title.

Basically, I have two classes, Foo and Bar. Foo has a property of type Bar. Bar has a Set<Int> on it.

data class Foo(bar: Bar)

data class Bar(ints: Set<Int>)

When I have an instance of Foo, and I want to add an Int to the set of Ints on bar, how do I do that in an idiomatic, immutable way that isn’t also ugly and confusing?

This is what I’ve got so far:

val myNewInt = 5
myFooDAO.save(foo.copy(bar = foo.bar.copy(ints = foo.bar.ints + setOf(myNewInt))))

Pretty awful, right? Unfortunately, I can only really think of two solutions.

  1. Add a function to Foo to add a new Int to the set of Ints on Bar, then basically move all of that horribly ugly copy code into that function. This basically hides the ugliness from where I’m using it, and makes the code easier to read, but that ugliness still exists in my code base. This is my preferred solution.
  2. Just make things mutable. This is not my preferred solution.

However, both of those thoughts have made me start thinking; how do we update objects while using immutability? Are those two things just fundamentally incompatible? Or do I just not know enough about how to do things in an immutable way? I’m interested in whatever thoughts/knowledge everyone has on this!

(For those curious, this is what my actual code looks like):

when {
    hero.titles.extinction -> accountDAO.save(
        account.copy(
            gameStatistics = account.gameStatistics.copy(
                extinctionPlayerCount = setOf(playersInGame) + account.gameStatistics.extinctionPlayerCount
            )
        )
    )

    hero.titles.nightmare -> accountDAO.save(
        account.copy(
            gameStatistics = account.gameStatistics.copy(
                nightmarePlayerCount = setOf(playersInGame) + account.gameStatistics.nightmarePlayerCount
            )
        )
    )
}

That’s militates in favour of adding a kind of “C++ const” keyword; where save method could be written as:

fun save(const Account account) { ... }

But, it may be a hard feature to add, especially to keep Java compatibilities.

You are probably looking for the lenses pattern. You can use the one provided by Arrow.

1 Like

You can add an element to a collection, no need for an intermediate set:

    extinctionPlayerCount = account.gameStatistics.extinctionPlayerCount + playersInGame

Not exactly what you’re looking for, but at least it shortens the code a little bit.

Using Arrow you can do it this way:

@optics
data class Foo(val bar: Bar) {
	companion object
}

@optics
data class Bar(val ints: Set<Int>) {
	companion object
}

val foo1 = Foo(bar = Bar(ints = setOf(1, 2, 3)))
val myNewInt = 5
val foo2 = Foo.bar.ints.modify(foo1) { it + myNewInt }
val foo3 = foo1.copy {
	Foo.bar.ints.transform { it + myNewInt }
}
println(foo1) // Foo(bar=Bar(ints=[1, 2, 3]))
println(foo2) // Foo(bar=Bar(ints=[1, 2, 3, 5]))
println(foo3) // Foo(bar=Bar(ints=[1, 2, 3, 5]))

As for the 2nd example I don’t have a complete model provided here, but it should look more less like this:

accountDAO.save(account.copy {
	when {
		hero.titles.extinction -> Account.gameStatistics.extinctionPlayerCount.transform { it + playersInGame }
		hero.titles.nightmare -> Account.gameStatistics.nightmarePlayerCount.transform { it + playersInGame }
	}
})
2 Likes

I’m not sure how this helps. Forget about the DAO, the real question is more “how do I update a property on a sub-object without copying the parent and the child?”

That’s pretty cool… I was hoping for something within the standard library, not a third-party library with (possibly) extra compilation, but the way it works for “setting” nested values, but presumably still in an immutable way (IE using copy), does kind of look like what I want.

That doesn’t help; my goal is to add a value to the Set on the sub object.

val currentFoo = Foo(Bar(setOf(5)))

val newFoo = currentFoo.copy(bar = currentFoo.bar.copy(ints = currentFoo.bar.ints + setOf(7)))

// newFoo = Foo(bar=Bar(ints=[5, 7]))

EDIT: Wait I get what you mean. Instead of doing setOf(newNumber) I just add it to the existing Set. My apologies. Yeah it does help a little bit. :slight_smile: