Mutating items in a Set

I'm trying to mutate the items in a Set: (contrived example, the real code is more complex with a set of structs with a mutating function)

var items: Set = ["1", "2", "3"]
for index in items.indices {
	items[index] += "1"
}
print(items)

This provides the error Left side of mutating operator isn't mutable: subscript is get-only

If I change items: Set to items: Array then the code works fine.

Ideally I wouldn't have to use indices but I understand why they have to be used with an array as the for loop provides a copy of the item, so I imagined it would be the same with Set even though a Set doesn't have numeric indices.

Is there any way to mutating items in a Set?

The subscript is get-only as modifying items could create duplicates. So you need to remove the existing item first and then insert a new item

var items: Set = ["1", "2", "3"]
for item in items {
    items.remove(item)
    items.insert(item + "1")
}
print(items)

Please note that you could end up with a differently sized set after the loop

It might be even a better idea to create an empty set first and insert mapped elements to it

I'd do this to be on the safe side:

var items: Set = ["1", "2", "3"]
let newItems = items.indices.map { index in
    items[index] + "1"
}
items = Set(newItems)

This loop is guaranteed to duplicate the entire storage of the set, regardless of whether any duplicate entries occur. As soon as you mutate the set, copy-on-write gets triggered because there are two references to items.

Specifically, the line “for item in items” creates an (implicit, shallow, semantic) copy of items which is only ever used for the purpose of iteration. Then at the point of mutation, the existence of a second reference leads to CoW duplicating the storage.

The same thing happens if you loop over items.indices instead. I do not know of a way to reuse the storage and avoid a duplicate allocation when mutating all the elements of a set

Perhaps someone else might?

3 Likes

I don't think the operation of in-place mutation is well-defined for Sets, because you can potentially generate values that collide with each other.

As a work-around, just create a new Set with each value mapped to a new one:

var items: Set = ["1", "2", "3"]
items = items.reduce(into: Set()) { $0.insert($1 + "1") }
2 Likes

This is implemented as an initializer.

var items: Set = ["1", "2", "3"]
items = .init(items.lazy.map { $0 + "1" })
1 Like

This approach will allocate a separate array as well as a new set.

This is better because it does not introduce an extraneous array allocation, it only allocates the new set.

This is probably fine as well, but I’m always a little leery about things like .lazy.map because it can be hard to verify that the compiler really did choose the lazy version of map and not the one that returns an array. I’ve seen situations where laziness was silently and invisibly dropped for reasons that are very much not obvious at a glance.

4 Likes