Swift Regret: Type-based Overloading

Part of the Swift Regrets series.

When we were first developing Swift, many of the mainstream languages had type-based overloading (C++, Java, C#), and many didn’t (C, Python, Objective-C). How did Swift end up with it?

(The previous regret was an Objective-C feature that got pulled into Swift. This one’s a place where I think Objective-C got it right.)

Objective-C’s lack of type-based overloading, plus Cocoa’s naming conventions, led to what some people call the “muffin man” problem:

[mainViewController addChildViewController:infoViewController]
[self doYouKnowTheMuffinMan:theMuffinMan]

Objective-C also didn’t have operator overloading, though it did get customizable subscript operations a few years before Swift. That was important for appending strings, but also the “library defined in the language” goal of having integers and floats be normal structs.

I’d say those two problems, more than anything else, were what pushed us to include type-based overloading in Swift. It solved both those problems, it had precedent. Of course it can be abused, but that’s true of nearly every language feature.

And it actually had some positive unlooked-for effects (unforeseen by me, anyway). One was the “converting initializer” convention, where init(_:) meant “convert the unlabeled argument to this type in the most straightforward way”. This is extremely common for value types in idiomatic Swift code. Another useful trick (though with some drawbacks) is implementing a specific operation—say, subscript(_: Range) for slicing…and then exposing a general operation on top of that, subscript<R: RangeExpression>(_: R). You don’t have to name one of them “-Impl” or something like that.

But along with the usual—valid!—complaints (particularly “you need to use Quick Help to see which overload got called”), overloading created another problem: it drastically slows down type checking. Because Swift, unlike C++, Java, or C#, type-checks a whole statement at once. That’s important for something like let names = employees.map { $0.name }. In C++ (pre-14) you can’t leave out the closure argument type like that, and there are more complex examples that would still need explicit types in C++20.

(What’s so bad about writing explicit types? Could Swift have ditched whole-statement type-checking? Maybe, but let’s save that for another time…and meanwhile you can follow the type-checking folks like Holly Borla and Doug Gregor for ways current Swift continues to improve.)

So why does type-based overloading make type-checking slow? Sure, each overload set multiplies the number of solutions by N, but that’s not the problem in practice. No, it’s the combination with another type-checking feature: implicit conversions. Swift has tons of implicit conversions: subclass to superclass, concrete type to protocol or generic, and some weirder ones like value-to-Optional and Array-to-pointer. That means that ruling out overloads isn’t just a matter of checking whether two types are equal. There are tricks the compiler does to try to make this efficient (some of them not 100% consistent!) but unfortunately for you I am not a type-checking expert and my knowledge runs out around here.

Besides type checker performance, which I may be somewhat wrong about anyway, the biggest reason to remove type-based overloading is that people find it hard to understand once generics get involved, whether that’s guessing which of two generic functions will be called, or calling a method within a generic function and expecting it to use the concrete type (as it would in C++, but not Rust). Note that none of this applies to name-based overloading. It’s not surprising that append(_:) and append(contentsOf:) are different functions.

Aside: A silly reason why type-based overloading makes life harder: the unique way to identify a function has to include types now. Symbol names have to uniquely identify functions. So now you have to do complex name mangling. This was noticeable enough in binary size that Apple added a flag to strip Swift symbols from the classic debug symbol table (they compress well enough in the modern symbol table format). Ugh.

Also, shoutout to return-type overloading, a feature we added because it was possible. Most of the time we had reasons for things, intended use cases. This had none and I don’t think I’ve ever seen a good use of it, and it makes several things more complicated.

But back on track: what would Swift look like without type-based overloading? No converting initializers, so everything gets a little more verbose. We’d do custom operators through protocols and that would cover the most common cases. We’d put “-Impl” on “backing” methods.

And what about the Muffin Man? Swift’s naming conventions didn’t match Cocoa’s, and this was especially obvious whenever Cocoa used “object” where Swift would have a generic…and where the concrete type in Swift might be a struct! But stripping the type name from an argument label is still viable; it’s only a problem when it would be ambiguous. In those cases, yeah, the Swift compiler’d have to leave the type name on. Maybe we’d have more NS_SWIFT_NAMEs. I think overall though it would have been fine, and made the language just a little simpler all around. We’d probably have made some things a little more awkward elsewhere, but having argument labels makes name-based overloading go much further.

(And by the way, generics themselves are a big part of solving this problem. Instead of needing -initWithArray: and -initWithSet:, you can just have init<S: Sequence>(elements: S).)

Unlike some of these other regrets, I don’t think this is an obvious right choice in hindsight. Type-based overloading really does remove filler words sometimes, adding “clarity at point of use”. But on the whole, on balance…