Swift History: Assignment Methods

Part of the Swift Regrets series.

To finish out the series I’m going to talk about something that’s neither a regret nor a delight, but a feature we didn’t end up including in Swift at all: assignment methods. These were a neat idea from Dave Abrahams (with help from Joe Groff) that predated the Swift Evolution Process, and indeed predated Swift 1.0. The proposal is still archived in the repo (and pretty easy to read) but I’m going to talk about it here cause I think it was cool.

The idea was that mutating and non-mutating methods are often paired: Array.append/appending, Set.formUnion/union. We have naming conventions for these in the API Design Guidelines, but they don’t quite feel optimal (and may not be obvious to non-native English speakers).

A big shoutout to Dave for curating and writing most of the API Design Guidelines, by the way, which probably qualify as a Swift Delight in their own right. Especially the “Fundamentals”, which really apply to any language.

Anyway, there’s one place where the mutating/non-mutating conflict was resolved a long time ago: binary operators. We have +/+=, &/&=, !/!= …okay, not that last one.

The idea was to extend that to methods. You’d have Array.append and Array.=append, Set.union and Set.=union. The use site shows why this spelling makes sense:

x += y
x  = x + y

x .= append(y)
x  = x . append(y)
x = x.append(y)

(Hopefully that’s convincing?)

It was a nifty idea, and it meant you got two methods for the price of one. An assignment method can be called without = to build up a new value copied from self; a non-assignment method that returns Self can be called with = to replace self at the end of the call.

The trouble is, what if you implement both? This is something you might do for performance: if you’re appending two arrays, you can allocate a buffer exactly big enough for both; if you’re appending one array to another, you want to first try reusing the buffer you already have. (With Swift’s copy-on-write-if-needed types you can jam both optimizations into the assignment method, but I don’t have a better example on hand.) But if you implement both, suddenly there’s a performance question at the call site where the user might care whether they’re using append or =append. (This does in fact come up in C++ and Rust, where you must independently implement + and += if you want to support both.) We talked about whether the optimizer could choose the best option, as in rewriting a + b to a += b if a was no longer used, and possibly the other way around (probably less important). But that seemed like a pretty non-intuitive change if a user wrote both functions.

Really, though, there are two places where this model falls down. One is with class instances, where a + b returns an independent object but a += b modifies a shared object. (See the earlier unresolved regret about classes vs. mutating.)

The other is with existing mutating functions, which modify a value in-place without that leading =. Would that now be confusing? Should all mutating methods be spelled with = now? And more importantly, what about mutating methods that return values, like Dictionary.removeValue(forKey:)? It’s really confusing in C when people do things like int x = (y += z); I don’t want to bring that back.

I can see solutions for the first two problems in another language—“you can’t implement both versions” and “don’t have built-in reference types (like Rust)”—but the third still stymies me. Mutating methods that return values are useful, especially for state machines.

Assignment methods were designed, implemented, and then removed. They solved some problems, but they had problems of their own, and they made the language bigger. Sometimes ideas don’t work out, and that’s okay…and Dave also points out that we didn’t really get enough time to try them out properly, at least partly thanks to management, and that they could have been successful after all. So I could see this one popping up in another language some day.