Nice way of copying an immutable value while changing only a few of its many properties?

Contents:

  1. A description of a situation I've found myself in enough times to write here about it.
  2. My way of handling it.
  3. The question "Have I invented this hacky solution because I'm missing something obvious?".
  4. A happy observation about improvements in optimization in a recent snapshot.

So here's the situation. Let's say there's this thing Foo:

struct Foo {
    var clasterId: String
    var nanoHopplerLength: Double
    var flapsterity: Float
    var isSingleWave: Bool
    var zapsterCount: Int
}

And we have someFoo:

let someFoo = Foo(clasterId: "abc",
                  nanoHopplerLength: 1.23,
                  flapsterity: 45.6,
                  isSingleWave: true,
                  zapsterCount: 7)

Now we happen to need anotherFoo that is just like someFoo, except for
zapsterCount which should be incremented by 8 and
isSingleWave which should be false.

How do we do that?

Alternative 1

let anotherFoo1 = Foo(clasterId: someFoo.clasterId,
                      nanoHopplerLength: someFoo.nanoHopplerLength,
                      flapsterity: someFoo.flapsterity,
                      isSingleWave: false,
                      zapsterCount: someFoo.zapsterCount + 8)

That's very verbose, and it would be even worse if Foo had more properties and we only needed one of them to change. (Now it's at least 2 out of 5, could be eg 1 out of 7!)

Alternative 2

var tmpMutableFoo = someFoo
tmpMutableFoo.zapsterCount = someFoo.zapsterCount + 8
tmpMutableFoo.isSingleWave = false
let anotherFoo2 = tmpMutableFoo

Perhaps a little better, but now we have that temporary mutable copy of someFoo in our scope. We could get it out of the scope using an immediately invoked closure:

Alternative 3

let anotherFoo3: Foo = {
    var tmf = someFoo
    tmf.zapsterCount = someFoo.zapsterCount + 8
    tmf.isSingleWave = false
    return tmf
}()

But that's not very nice either.

It feels like I'm missing some super obvious simpler way (am I? :open_mouth:), I mean something similar to but better than eg:

Alternative 4

let anotherFoo4 = someFoo
    .setting(\.zapsterCount, to: someFoo.zapsterCount + 8)
    .setting(\.isSingleWave, to: false)

or

Alternative 5

let anotherFoo5 = someFoo .= (\.zapsterCount, someFoo.zapsterCount + 8)
                          .= (\.isSingleWave, false)

which is possible with this:

protocol KeyPathSettable {}
extension KeyPathSettable {
    func setting<V>(_ keyPath: WritableKeyPath<Self, V>, to value: V) -> Self {
        var result = self
        result[keyPath: keyPath] = value
        return result
    }
    static func .=<V>(lhs: Self, rhs: (WritableKeyPath<Self, V>, V)) -> Self {
        return lhs.setting(rhs.0, to: rhs.1)
    }
}
infix operator .= : AdditionPrecedence


extension Foo : KeyPathSettable {}

Btw, I noticed that with a recent snapshot (2020-01-21) of the compiler (and -O), all alternatives seem to compile down to identical binary code (at least in my quick test). The default toolchain of Xcode 11.3.1 (11C504) does not, however.

2 Likes

Why should the mutable copy be temporary? Why not leave it mutable?

Once it gets lowered to SSA, they're all going to look like this anyway, so you won't get better performance than this one.

I'm not sure I understand what you mean. If you mean I should leave tmpMutableFoo mutable, then well, that is Alternative 2, which is fine, unless you feel that it shouldn't be necessary to declare a new variable every time you need to do this kind of thing (assuming all copy-with-little-modifications are not of the same type, so the mutable variable cannot be reused).

If you mean why I wouldn't want to simply do this:

var anotherFoo2b = someFoo
anotherFoo2b.zapsterCount = someFoo.zapsterCount + 8
anotherFoo2b.isSingleWave = false

then, depending on how anotherFoo2b is used, it's the same as asking "why ever use let and not just use var?".

Something like this has been discussed here before, under the name β€œwith”, but that’s probably impossible to search for.

3 Likes

Ah, I actually called it with(:) and Withable when I first wrote it.


Trying to search the forums for Withable, I found this:

Yes, that's what I mean. Your problem was:

Now we happen to need anotherFoo that is just like someFoo , except for
zapsterCount which should be incremented by 8 and
isSingleWave which should be false .

So what you should do is make a copy of someFoo, called anotherFoo, increment zapsterCount by 8 and assign isSingleWave to false. Just like you wrote it, basically.

I don't buy in to all this with and fancy syntax sugar. There are some who insist on shoving functional programming in to any and every hole, even for the very simplest of problems. I'm more of the "right tool for the right job" kind of person, and in this case there's absolutely nothing wrong with the obvious solution. There is also such a thing as over-engineering...

There is a use-case for let: when you initialise a variable and never change it. You initialised a variable, but then changed a couple of properties. It's a var :man_shrugging:

1 Like

Do you think there is any difference in the benefit that could be gained by changing var to let in

var x = foo()
// Never mutate x

compared to in

var x = foo()
mutate(&x)
// Never mutate x after this point

(where the change in the second would involve adding a with or other construct to allow the initial mutation)

To me, the change has equal benefit in both, so my answer to "why not just leave it a var" is the same as the answer to "why use let at all"

If you disagree, I'm curious as to what benefit the use of let is getting you in the first case that it wouldn't also get you in the second (aside from "the compiler stops complaining at me")

2 Likes

Hmm I think I remember seeing it somewhere else closer to

func with<T>(_ t: T, _ body: (inout T) throws -> ()) rethrows -> T {
	var t = t
	try body(&t)
	return t
}

which would allow you to do

let anotherFoo6 = with(someFoo) {
	$0.zapsterCount += 8
	$0.isSingleWave = false
}
4 Likes

OK, so we agree that there is a use-case for let. And that's exactly the use-case I describe:
The let in
let anotherFooX = ...
implies that anotherFooX should never change after the initialization.
Here's the first alternative again:

Clearly, the use-case here is to

I have the same kind of requirements and this is the best solution I've found so far. It would be better if we could extend an arbitrary type in Swift (so we could define with as a method), but that's it for now.

But we can extend an arbitrary type in Swift so that it gets with as a method, ie:

protocol CopyableWithModification {}
extension CopyableWithModification {
    func with(_ modify: (inout Self) throws -> Void) rethrows -> Self {
        var copy = self
        try modify(&copy)
        return copy
    }
}
extension Foo : CopyableWithModification {} // (<-- Foo can be any type.)

which will let us write:

let anotherFoo7 = someFoo.with { $0.zapsterCount += 8
                                 $0.isSingleWave = false }

(I've included the equivalent Alternative 1 here, for comparison.)
let anotherFoo1 = Foo(clasterId: someFoo.clasterId,
                      nanoHopplerLength: someFoo.nanoHopplerLength,
                      flapsterity: someFoo.flapsterity,
                      isSingleWave: false,
                      zapsterCount: someFoo.zapsterCount + 8)

Anyway, I'm just saying that any type can be extended like this to get with as a method. But perhaps that's not what you meant?

Not that hard to find it if you remember who wrote the post: Circling back to `with`

3 Likes

I meant something a little different, that is, "extension" as in "adding a method to an arbitrary type", rather than "defining a protocol and making an arbitrary type conform to it".

For example, in pseudo-swift:

extension Any {
    func with(_ modify: (inout Self) throws -> Void) rethrows -> Self {
        var copy = self
        try modify(&copy)
        return copy
    }
}

This would add a with method to everything, without the need to define a protocol and manually conforming types to it. I understand that writing extension Foo : CopyableWithModification {} for each type doesn't require much effort, but I still find easier and faster to just use a global with function.

2 Likes

3:30am so not much brain left "today", but the OP question made me think of:
make an initializer for the struct that takes an instance of self as the first param and then all the normal init params as optionals with default value of nil then you could create a new one with zero or more params modified.

Bit verbose to create, but nice to use once you have it (macOS Swift playground, Swift 5.1):

import Cocoa

struct Foo {
	var name: String
	var age: Int
	var height: Double

      // unfortunately if you make one custom init you lose the auto-generated one
      // so we have to write that as well:
	init(name: String, age: Int, height: Double) {
		self.name = name
		self.age = age
		self.height = height
	}

      // initialize from an existing instance with optional customization of any property
	init(from: Foo, name: String? = nil, age: Int? = nil, height: Double? = nil) {
		self.init(name: name ?? from.name,
				  age: age ?? from.age,
				  height: height ?? from.height)
	}
}

  // make a Foo:
let a = Foo(name: "Joe", age: 20, height: 1.5)
print(a)

  // simple copy; b = a is easier :)
let b = Foo(from: a)
print(b)

 // copy but change one param, age.
let c = Foo(from: a, age: 10)
print(c)

  // change two params
let d = Foo(from: a, age: 10, height: 1.3)
print(d)

output is:

Foo(name: "Joe", age: 20, height: 1.5)

Foo(name: "Joe", age: 20, height: 1.5)

Foo(name: "Joe", age: 10, height: 1.5)

Foo(name: "Joe", age: 10, height: 1.3)

Yes, that was the first solution I went for (a long time ago), but since it involves so much typing for each type I wanted to use it on, I started using something like what I described in the OP, and since then I've changed to this:

protocol CopyableWithModification {}
extension CopyableWithModification {
    func with(_ modify: (inout Self) throws -> Void) rethrows -> Self {
        var copy = self
        try modify(&copy)
        return copy
    }
}

Having that protocol means we'll only need to write one line for any type we want to use it on:

extension Foo : CopyableWithModification {}

and the call site actually becomes less verbose than the init-method too, eg:

let anotherFoo = someFoo.with { $0.zapsterCount += 8
                                $0.isSingleWave = false }

compared to:

let anotherFoo = Foo(from: someFoo,
                     zapsterCount: someFoo.zapsterCount + 8,
                     isSingleWave: false)

This method requires mutable properties though, which the init-method doesn't.

1 Like

initialise a variable and never change it.

The same also applies to auto generated member init(), in order to make use of it, you must declare the member field as var, cannot be let:

struct Blah {
    let a: Int

    // you have to write the init
    init(_ a: Int) {
        self.a = a
    }
}

But if you declare field as var, no need to write the init():

struct Blah {
    var a: Int
}

Can we not have both? declare let a: Int and make use of auto gen'ed init()

That's not correct. var and let variables are part of the auto-generated initializer, but when you added your own init inside the type declaration, the auto-generated initializer was lost. Moving your init to an extension will let you keep both.

3 Likes

You are right, my example is no good. I did encountered this problem with some SwiftUI something Binding such that I cannot declare it as let and use auto gen'ed init()...if I can find my code, I'll post.

Yes, @Binding annotated variables must be var due to how property wrappers work. How that behavior interacts with the auto-generated init I'm not sure.

I remember now: its let with default value that can be overridden but using only the auto gen'ed init():

struct Blah {
     let a = 100
}

let x = Blah(a: 999).   // <== error: argument passed to call that takes no arguments

but it's okay if you use var:

struct Blah {
     var a = 100
}

let x = Blah(a: 999)

But I really want this:

struct Blah {
     let a: Int

     init(a: Int = 100) {
         self.a = a
     }
}

let x = Blah(a: 999)

So I must write the init()

(I remembered Binding because I didn't like having to hand code the init() and deal with setting the bindings)