swiftimmutability

How to copy a struct and modify one of its properties at the same time?


If I want to represent my view controller's state as a single struct and then implement an undo mechanism, how would I change, say, one property on the struct and, at the same time, get a copy of the the previous state?

struct A {
   let a: Int
   let b: Int

   init(a: Int = 2, b: Int = 3) {
      self.a = a
      self.b = b
   }
}

let state = A()

Now I want a copy of state but with b = 4. How can I do this without constructing a new object and having to specify a value for every property?


Solution

  • The answers here are ridiculous, especially in case struct's members change.

    Let's understand how Swift works.

    When a struct is set from one variable to another, the struct is automatically cloned in the new variable, i.e. the same structs are not related to each other.

    struct A {
        let x: Int
        var y: Int
    }
    
    let a = A(x: 5, y: 10)
    var a1 = a
    a1.y = 69
    print("a.y = \(a.y); a1.y = \(a1.y)") // prints "a.y = 10; a1.y = 69"
    

    Keep in mind though that members in struct must be marked as var, not let, if you plan to change them.

    More info here: https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html

    That's good, but if you still want to copy and modify in one line, add this function to your struct:

    func changing<T>(path: WritableKeyPath<A, T>, to value: T) -> A {
        var clone = self
        clone[keyPath: path] = value
        return clone
    }
    

    Now the example from before changes to this:

    let a = A(x: 5, y: 10)
    let a1 = a.changing(path: \.y, to: 69)
    print("a.y = \(a.y); a1.y = \(a1.y)") // prints "a.y = 10; a1.y = 69"
    

    I see that adding 'changing' to a lot of struct would be painful, but an extension would be great:

    protocol Changeable {}
    
    extension Changeable {
        func changing<T>(path: WritableKeyPath<Self, T>, to value: T) -> Self {
            var clone = self
            clone[keyPath: path] = value
            return clone
        }
    }
    

    Extend your struct with 'Changeable' and you will have your 'changing' function.

    With the 'changing' function approach, too, any property that you specify in the 'changing' function's call sites (i.e. of type WritableKeyPath) should be marked in the original struct as var, not let.