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

7

47

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?

Fordo answered 12/7, 2016 at 14:9 Comment(15)
let state2 = state; state2.b = 4Steeple
But b is immutable.Fordo
b is immutable contradicts how would I change, say, one property on the struct. Unless you're looking for a way to initialize a new struct with the properties of the previous one except some of them?Corena
Make b mutable :D or you must construct new instance in order. You can perform some initializer of course, but is it easier?Steeple
@EricD Yes, that's what I'm trying to do - initialize a new struct with the properties of the previous one except some.Fordo
Why do you think the properties of A need to be let instead of var?Romo
@robmayoff They don't need to be, but its said that immutability makes code safer.Fordo
If only Swift had Kotlin's convenient way of doing this...Chemotherapy
@BryanBryce How does Kotlin do it?Fordo
@IanWarburton val jack = User(name = "Jack", age = 1) val olderJack = jack.copy(age = 2)Chemotherapy
@IanWarburton A copy function is made for all DataClass types (Kotlin's version of a struct) and that function has optional parameters for each property.Chemotherapy
It's so irritating that Swift doesn't have the convenience mentioned above, all other similar langs have it, Kotlin, Rust, Scala, Typescript...Gunk
Javascript has the spread operator e.g. { ...obj }Fordo
Swift has structs so it doesn't need any copy methods, unlike Kotlin, Javascript etc. See my answerStorm
You could define a mutating func in the struct and modify its properties, then return self. This will return the origin object but not a copy.Prostyle
S
42

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.

Storm answered 14/3, 2021 at 10:46 Comment(8)
I like the extension and setting via WriteableKeyPath, but this does still require the struct members to be declared as mutable var... which the question is (partially) trying to avoid.Muenster
True, but the struct must be in 'var' variable, in order to make it mutable, therefore there is no need to worry, coz u can't just accidently mutate it.Storm
The drawback of this approach is that you are limited to change a single property but considering the question title that's fine.Rolfston
@LeoDabus what about variadic parameter?Storm
@ArutyunEnfendzhyan not sure what you mean by thatRolfston
@LeoDabus sorry, i didn't mean variadic parameter. I mean, you want to change multiple parameters, you can chain the .change the callsStorm
I would consider naming the method changing(path:to:) instead of change(path:to:), as this is a non-mutating method (returns a copy with a property modified), in order to match the Swift API design naming guidelines. swift.org/documentation/api-design-guidelines. This is similar to the distinctions b/w Array's appending vs. append.Validate
Answer calls other answers ridiculous and says this members in struct must be marked as var, not let, if you plan to change them.Manipur
S
35

Note, that while you use placeholder values for constants a and b you are not able to construct instance of A with any other values but this placeholders. Write initializer instead. You may write custom method that change any value in struct also:

struct A {
    let a: Int
    let b: Int

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

    func changeValues(a: Int? = nil, b: Int? = nil) -> A {
        return A(a: a ?? self.a, b: b ?? self.b)
    }
}

let state = A()
let state2 = state.changeValues(b: 4)
Steeple answered 12/7, 2016 at 14:38 Comment(3)
I've updated the question to remove the placeholders.Fordo
Seems a shame that the logic in your answer isn't part of the language given that Swift is good at making copies of values.Fordo
Be careful using this method! It won't work if one of your properties is nullable and you want to set that property to nil in the new copy.Restrictive
E
17

If you can live with the properties being mutable, this is an alternative approach. Advantage is that it works for every struct and there's no need to change the function upon adding a property:

struct A {
    var a: Int
    var b: Int

    func changing(change: (inout A) -> Void) -> A {
        var a = self
        change(&a)
        return a
    }
}

let state = A(a: 2, b: 3)

let nextState = state.changing{ $0.b = 4 }

You could also have an extension for this:

protocol Changeable {}
extension Changeable {
    func changing(change: (inout Self) -> Void) -> Self {
        var a = self
        change(&a)
        return a
    }
}

extension A : Changeable {}

Also you can use this to do it without any additional code:

let nextState = {
    var a = state
    a.b = 4
    return a
}()

And if you don't mind the new struct being mutable, it's just

var nextState = state
nextState.b = 4
Elatia answered 12/7, 2016 at 14:50 Comment(3)
Why not, as @Shadow of wrote, let state2 = state; state2.b = 4?Fordo
@IanWarburton Because you'd have to make state2 mutable to make it compile. I added a simpler one if you don't want any additional code, while not being as pretty.Elatia
Sensible thing to do is, as you suggest, make things mutable and, in order to create a new undo state, push the instance onto an "undo" stack before modifying it.Fordo
M
6

You can write function "copy" for your struct like in Kotlin

struct A {
   let a: Int
   let b: Int

    func copy(a: Int? = nil, b: Int? = nil) -> A {
        .init( a: a ?? self.a,
               b: b ?? self.b
        )
    }
}

let state = A(a: 3, b: 5)
let stateCopy = state.copy()
let stateCopy2 = state.copy(a: 4)
let stateCopy3 = stateCopy2.copy(b: 8)
let stateCopy4 = stateCopy2.copy(a:1, b: 2)
Monkeypot answered 28/12, 2022 at 18:37 Comment(0)
E
4

I really like @Shadow answer, but I had a hard time adapting it to a scenario were the fields of the struct could be nullable, so I decided to use the builder pattern instead. My code looks something like this:

struct A {
    let a: Int?
    let b: Int?

    class Builder {
        private var a: Int?
        private var b: Int?

        init(struct: A) {
            self.a = struct.a
            self.b = struct.b
        }

        func build() -> A {
            return A(a: self.a, b: self.b)
        }

        func withA(_ a: Int?) -> Builder {
            self.a = a
            return self
        }

        func withB(_ b: Int?) -> Builder {
            self.b = b
            return self
        }
    }
}

And then you can use it like:

A.Builder(struct: myA).withA(a).withB(nil).build()

With this my structs are really immutable, and I can quickly create a copy and change one or many of its field to be either nil or another Int

Encratis answered 24/8, 2018 at 10:31 Comment(0)
M
2

The best way I have found (that keeps the references as let rather than changing them to var), is to write an initializer method that takes an object of the same type to "copy", and then has optional parameters to set each individual property that you want to change.

The optional init parameters allow you to skip any property that you want to remain unchanged from the original struct.

struct Model {
    let a: String
    let b: String
    
    init(a: String, b: String) {
        self.a = a
        self.b = b
    }
    
    init(model: Model, a: String? = nil, b: String? = nil) {
        self.a = a ?? model.a
        self.b = b ?? model.b
    }
}

let model1 = Model(a: "foo", b: "bar")
let model2 = Model(model: model1, b: "baz")

// Output:
// model1: {a:"foo", b:"bar"}
// model2: {a:"foo", b:"baz"}
Muenster answered 3/10, 2020 at 5:36 Comment(3)
With big structs i got a stack overflow crash with same way of implementation.Gould
That is not even close to be a fine solution let alone "the best"Manipur
@Manipur suggest another solution then that uses let constants.Muenster
A
2

As of Swift 5.9 with the introduction of Variadic Types, we can tweak @arutyun-enfendzhyan's answer to allow multiple changes at once:

Our base struct

struct Test {
    // If the values can change, they should be declared as vars
    var string = String()
    var int = Int()
    var double = Double()
}

Our helper

struct KeyValuePair<Model, Value> {
    let key: WritableKeyPath<Model, Value>
    let value: Value
    
    func update(_ model: inout Model) {
        model[keyPath: key] = value
    }
}

The implementation

extension Test {
    func copy<each Value>(
        with pair: repeat KeyValuePair<Self, each Value>
    ) -> Self {
        var clone = self
        repeat (each pair).update(&clone)
        return clone
    }
}

let test = Test()
test.copy(with:
    .init(key: \.string, value: "string"), 
    .init(key: \.int, value: 10), 
    .init(key: \.double, value: 20)
)

>> Test(string: "string", int: 10, double: 20.0)

There are other options too:

extension Test {
    func copy<each Value>(
        keys: repeat WritableKeyPath<Self, each Value>,
        values: repeat each Value
    ) -> Self {
        var clone = self
        repeat KeyValuePair(key: each keys, value: each values).update(model: &clone)
        return clone
    }
}

let test = Test()
test.copy(keys: \.string, \.int, \.double, values: "string", 10, 20.0)

>> Test(string: "string", int: 10, double: 20.0)
extension Test {
    func copy<each Value>(
        with tuple: repeat (key: WritableKeyPath<Self, each Value>, value: each Value)
    ) -> Self {
        var clone = self
        repeat KeyValuePair(key: (each tuple).key, value: (each tuple).value).update(model: &clone)
        return clone
    }
}

let test = Test()
test.copy(with: (\.string, "string"), (\.int, 10), (\.double, 20.0))

>> Test(string: "string", int: 10, double: 20.0)

Now about the Changeable protocol...

It would be awesome if we could add all of this to the protocol the same way @arutyun-enfendzhyan did. But there seems to be a limitation with variadic types and protocol extensions.

We can see the error as follows:

protocol Changeable {}

extension Changeable {
    func copy<each Value>(
        with pair: repeat KeyValuePair<Self, each Value>
    ) -> Self {
        var clone = self
        repeat (each pair).update(&clone) // Type of expression is ambiguous without more context
        return clone
    }
}

Which is a shame, because we would need to copy and paste some code instead of reusing it with a simple protocol.

If anyone has other ideas on how to make use of protocols here, feel free to add a comment!


Update

As of the official release of Swift 5.9 and Xcode 15, this seems to be working within an xcode project, but still crashes my playground. Full code:

import Foundation

protocol Changeable {}

extension Changeable {
    func copy<each Value>(
        with pair: repeat KeyValuePair<Self, each Value>
    ) -> Self {
        var clone = self
        repeat (each pair).update(&clone)
        return clone
    }
}

struct KeyValuePair<Model, Value> {
    let key: WritableKeyPath<Model, Value>
    let value: Value
    
    func update(_ model: inout Model) {
        model[keyPath: key] = value
    }
}

struct Test: Changeable {
    var string = String()
    var int = Int()
    var double = Double()
}

let test = Test()
test.copy(with:
    .init(key: \.double, value: 1),
    .init(key: \.int, value: 2),
    .init(key: \.string, value: "3")
)

>> Test(string: "3", int: 2, double: 1.0)
Americanism answered 15/7, 2023 at 5:36 Comment(1)
Mind you, this was just a fun little experiment using variadic types. I'd still go for the @arutyun-enfendzhyan approach as it's simpler and more reliable.Americanism

© 2022 - 2024 — McMap. All rights reserved.