Run-time Polymorphism in Swift
This has come up several times on the forums over the years, but I’ve never written it up in a standard place, so here it is: There are only three ways to get run-time polymorphism in Swift. Well, three and a half.
What do I mean by run-time polymorphism? I mean a function/method call (or variable or subscript access) that will (potentially) run different code each time the call happens. This is by contrast with many, even most other function calls: when you call Array’s append
, it’s always the same method that gets called.
So, what are the three, sorry, three and a half ways to get this behavior?
- Calling a function value (closure)
- Calling a class member
- Calling a protocol requirement
- Manually testing the type of a value
Calling a function value
This one’s kind of obvious. If you’re calling a callback, it can be anything that matches the function type, depending on where it’s coming from.
Calling a class member
A non-final
method on a non-final
class may be overridden in subclasses, so calling a class method does dynamic dispatch based on the run-time type of self
. This is the most familiar, object-oriented notion of “polymorphism”, and it’s usually not surprising to people.
Note that “class method” is kind of ambiguous: this rule applies to instance methods and type-level methods (and to properties, subscripts, and required
initializers). In a class, however, static
is equivalent to class
plus final
, so in that case there won’t be any dynamic dispatch.
There’s actually one more place where class members are dynamically dispatched, and that’s the very specific case of convenience initializers. The call to self.init
within a convenience initializer is dynamically dispatched, which is why convenience initializers are only inherited if you provide all the non-convenience initializers of your superclass. This is a pattern from Objective-C turned language feature in Swift, and yeah, maybe it’s more complexity than we really needed.
Calling a protocol requirement
This one’s also not too surprising; after all, the whole point of protocols is that they provide a common API implemented by concrete types. What might be surprising is that members added in extensions to the protocol do not get to participate in this behavior. If you think about it, though, supporting that would mean that at run time the program would have to look at all the possible methods a concrete type has and see if any of them match this extension method. If more than one matched, the runtime system would have to perform overload resolution. And what if that comes out ambiguous? So no, extension methods are either chosen directly at compile time, or passed over directly at compile time; whether they get called is entirely determined by static type information, not run-time polymorphism.
Manually testing the type of a value
This doesn’t really count, but it’s here because sometimes it really is the best answer to a problem. Swift does not provide perfect parametricity; you can attempt to downcast/convert to a more specific type whenever you want with is
and as?
expressions, or with is
and as
patterns:
if firstPet is Cat, let secondPet = secondPet as? Dog { /* … */ }
switch thirdPet {
case is Cat:
/* … */
case let thirdPet as Dog:
/* … */
default:
break
}
Whether or not this is a good idea is partly a matter of tradeoffs and partly of taste, but Swift does allow it.
Aside: What about generics?
Generics are a powerful and flexible tool, but in general they don’t result in any more run-time polymorphism than any
types (formerly “protocol composition types”). This often throws people who are used to C++ templates, where overload resolution is done on the concrete type that satisfies the generic constraints rather than on the generic type. Swift didn’t choose that option for two main reasons: it makes it much harder to diagnose issues at compile time, and it means that the entire body of the generic has to be visible to callers (so they can substitute in the concrete type). This is good for optimization, but bad for library evolution. You can think of Swift’s model as “the decision of which overload to call is made based on the knowledge where the call is written, which in this case is inside a generic function with certain constraints”.
I don’t know of any other modern languages that have templates like C++, but there’s still a choice between monomorphization, i.e. generating a separate copy of the code for every concrete type, and polymorphic generics, where a single copy of the code uses dynamic dispatch to work on many different types.1 Different languages take different approaches to this:
Language | Generics are… | Generic types are… | Overloads are resolved… |
---|---|---|---|
C++ | monomorphized | expanded into concrete types | on the concrete types (hence “template”) |
Rust | monomorphized | expanded into concrete types | based on constraints |
Swift | polymorphic | expanded into concrete types (but sometimes indirected) | based on constraints |
Java | polymorphic | “erased” to their constraints | based on constraints |
Objective‑C | polymorphic | “erased” to their constraints | what’s an overload2 |
There is now a way to get C++-like behavior in Swift (and Rust): macros. But Swift’s macros are entirely syntactic and have to be invoked explicitly, so they don’t naturally lend themselves to C++ template-style usage, at least not today. So sometimes instead this is where the “3.5” solution comes into play: a dynamic cast inside the body of a generic method acts as a form of “specialization”, even though it does have a checking cost at run time.
Takeaways
So let me re-iterate: the three-and-a-half features listed at the top are the only forms of run-time polymorphism in Swift. Now when someone asks “how can I allow arbitrary different argument types to result in different behavior”, you know the answer: make a protocol. (Or piggyback on a base class, if they’re already classes in a hierarchy you own.) When someone asks “why didn’t this method call pick the more specialized overload”, you know the answer: generics aren’t templates, overload resolution happens based on the generic constraints alone, and they may want to make a protocol. And when someone asks “hey, how do I make my protocol extension methods overridable?”, you know the answer: you have to make another protocol (and possibly downcast to it from the original type).
…Look, the tagline “protocol-oriented programming” may be a bit of a buzzword, but we weren’t kidding! Protocols are your tool for run-time polymorphism, and polymorphism in general, that works on value types. Use them!
-
Optimizations blur these distinctions. C++ or Rust code may generate many copies of the same function, but then optimize them back into one function if they have the same behavior at the machine code level, or at least outline the common parts of the function to save on code size. Like inlining, this is something that’s often based on heuristics and other settings, and is still an area of active development (or at least was a few years ago). Conversely, while Swift formally uses a single definition for every version of a generic function, it sometimes specializes them for particular concrete types to increase performance at the cost of code size. ↩︎
-
I joke, but actually Objective-C does care about doing some type-checking on method calls for generics, to get the right calling convention. You just can’t call a method that’s declared to return
float
in one place andid
in another, and so the compiler will complain if you try to call a method that’s not in the parameter’s constraints at all.I also snuck in Rust overloads. Rust does have overloading, even though it pretends not to: separate traits can declare methods with the same name, and a caller is required to disambiguate between them manually if both traits are valid. ↩︎