I saw this in some code and assumed it would not work - but tested it in a test project and it did:
var stuff: [String: [Int]] = [:]
let i: [Int] = [1, 2, 3, 4, 5]
stuff["Fred"] = i
stuff["Fred"]?.append(1000)
print("STUFF:", stuff) // STUFF: ["Fred": [1, 2, 3, 4, 5, 1000]]
You could have knocked me over with a feather! My long standing belief was that the value - an Array - is a reference value not an object. Thus, I was 100% convinced that I had to:
if var array = stuff["Fred] {
array.append(1000)
stuff["Fred"] = array
}
I believe this is the power of the _modify accessor in the Dictionary subscript which yields the content for the key to allow for modification. I could be wrong, so I'll let others fill in the details on how _modify works.
Even without _modify the end behavior would be the same. The mutatingappend method would get the value from the dictionary, modify it, and then set it again. And Array is implemented as a copy-on-write type, so you can think of append as being implemented as “allocate a new buffer, copy the values from the old buffer into it, add the new value, and modify self to refer to the new buffer instead of the old one”.
(Fun fact: at least part of the motivation for this “writeback” feature was making window.frame.origin += 10 work for UI programming.)
Swift's subscript is the magic at play here. It works more or less like a variable with getter and setter:
var foo: [Int] {
get {
stuff["someKey"]
}
set {
stuff["someKey"] = newValue
}
}
Note that swift's function doesn't have this superpowers as it returns immutable value:
func bar(_ key: String) -> [Int] {
stuff[key]
}
bar("Fred").append(1000) // 🛑 cannot use mutating member on immutable value: function call returns immutable value
bar("Fred") = [1, 2, 3] // 🛑 expression is not assignable: function call returns immutable value
Theoretically swift could allow this syntax:
func bar(_ key: String) -> [Int] {
get {
stuff[key]
}
set {
stuff[key] = newValue
}
}
making the above erroneous mutations possible. Then we'd probably won't need a separate notion of subscripts.
As we'll likely never get named subscripts, and instance subscript get accessors are made obsolete by callAsFunction, extending functions to be settable is the way forward. I haven't seen a proposal though.
@jrose's answer is right on. For clarity to readers coming to this thread in future, copying out an element to mutate it has never been necessary in any public version of Swift. Supporting the observed behavior doesn't require the _modify accessor and doesn't require any "magic," and you do not need to rely on any undocumented language features, and no new proposal is necessary, to make this work for your own subscripts and types.
it does require the _modify accessor, otherwise the append algorithm becomes quadratic in complexity.
that OP also wrote a quadratic algorithm here does not mean that exposing an array (or other collection type) property as a settable property is a good idea.
@tera is right; the way to understand this is that subscripts and variables are both kinds of storage, and uses of them are storage reference expressions whose semantics depend on how they are used (or accessed). Accesses are classified as either reads, writes, or read-writes in a fairly straightforward recursive way:
Assigning to storage is a write.
Passing storage inout (or calling a mutating method on it, or constructing a mutable pointer from it) is a read-write.
Accessing member storage (a property or subscript) is a read-write if:
the member is a stored property of a value type and the member access is a write or a read-write or
accessing the member requires calling a mutating accessor.
All other storage accesses are reads.
Swift does not silently drop mutations; if you write something that looks like a mutation of storage, that will not type-check unless the mutation actually goes back into the original storage. Whether that happens sufficiently "in place" to avoid triggering copy-on-write is a more complicated question.
If Swift ever adds general with functions that can yield values back to the caller before resuming, coroutine-style, there might be an evolution path for such calls to yield a mutable storage reference. But I do not think there is a path to ever treat calls as storage references that would have accessors and recursively propagate mutability to/from self like subscripts do. It is more likely that we would add named subscripts, although I continue to believe that it is better to do named subscripts by providing a meaningful view as a named property that happens to be subscriptable among other operations.
This is a harder problem that appears at first glance, because it would require nailing down the semantics of inout parameters, and throws/async setters, neither of which is something that subscripts support today.
This is additive functionality that can be addressed later on, no? If subscripts are removed in favour of functions with setters we'd have this:
"named" subscript functionality.
fewer "[ ]" brackets (I bet some people would miss those).
somewhat simpler language.
functions with setters initially could be restricted to match the limitations of today's subscripts (no inout/throws/async), this can be addressed later.
This may be rolled in phases, e.g. subscripts could be soft deprecated initially.