Swift

The Genius of Protocols

I’m a big fan of Swift — Ferrite Recording Studio, the big new app I’m working on, is written in it (more on that another day).

I’ve found it hard to articulate quite why I like Swift so much, though, because it comes down to a “sum of the parts” thing, where Swift has scavenged a bunch of great ideas from other languages and assembled them into a nice, relatively cohesive whole, rather than there being any single feature I can point to and say “There! That thing there is why!”

However, the closest I can come to a single feature (or maybe two) is how it treats protocols (and extensions, which you can use to declare protocol conformance). Mainly because these are the lynchpin for so much of the other things Swift does.

The funny thing, though, is that I keep seeing people complaining about them. I have a theory this is because Objective-C also has protocols, and they’re not the same thing, and this is causing cognitive dissonance. Sometimes I kinda wish Swift’s protocols were called something else, like “capabilities” maybe. But whatever names you throw around for them, I’m pretty happy with Swift’s idea of protocols. Especially since it also supports Objective-C’s idea of protocols, and all you have to do to use them is type 4 extra characters.

Protocols As They Are In Objective-C

Originally, Objective-C’s compiler let you call any method on any object, whether it supported it or not. If it didn’t, well, it’d crash at runtime, but the compiler didn’t care. Back then, “protocols” were barely a language feature at all, more of a way to describe, from one programmer to another, what methods an object was expected to support.

Over time, people realised this wasn’t a great idea, renamed those to “informal protocols”, and added “formal protocols”, which you declare for the compiler’s benefit. They are, again, just a list of methods (and properties, which are kinda just candy for a method or two anyway) that the object supports, but now, the compiler will complain if you screw up, and when you implement a protocol, autocomplete can fill out the method declarations for you.

In Objective-C, everything is dispatched dynamically, similarly to scripting languages like Python or Ruby. Essentially, the first time you call a method on a class, its name is looked up in the class being called, it searches through until it finds a match, and then gets the address, which is then cached to speed up future calls.

All of this depends on the Objective-C runtime, which means it can only be used on Objective-C objects, which are allocated on the heap and passed by reference, and those references are automatically refcounted by the runtime. Everything else is out of luck.

Protocols As They Are In Swift

Swift Protocols are similar in that they are lists of methods and properties (and in some cases related types/aliases) that something supports. However, the big differences are:

  • They’re dispatched statically (the compiler looks up the address at compile time) or virtually (the compiler looks up the Virtual Method Table offset at compile time, and bounces off it to get the real address at runtime) instead of dynamically (the compiler looks up nothing, the runtime searches for a string)
  • They support generics, which partially help make up for the limitations of not being dynamic
  • This is usually seen as being about efficiency, which, you know, it is a lot faster, but is also not the whole picture, because…
  • Not being dynamic, means Swift protocols don’t depend on the Objective-C runtime, which means you can apply them to things that are not Objective-C objects — which is huge!

This is the genius of protocols in Swift. You can write code that works with a protocol, and apply it to truly anything that supports the protocol.

A while back, I wrote a function that does a standard Longest Common Subsequence diff on two arrays. It does not give two craps about what those arrays contain, as long as it can check to see if two items are the same or different (ie they implement the Equatable protocol).

And because you can apply Equatable to all sorts of things, you can use this diff function on arrays of all sorts of things. They can be Swift classes, obviously. But they can also be Objective-C classes. Or Swift enums. Or integers, floats and bools. Or old-school C structs: you can add protocol conformance to vanilla C structs — including existing ones you aren’t in a position to actually change the source code to, like, say, sockaddr or kevent, or CGPoint.

Working with plain C structures is frequently a massive pain, so people often write “object oriented” wrappers around them to make them easier to work with. But then you run into leaky abstractions or impedance mismatch. In Swift, you can make the original raw C structs as easy to use as native Swift classes, and get the best of both worlds. Protocol conformance is a part of that.

So, yes, you can work with old-school BSD sockets code (this is a hypothetical example, don’t bug me about its utility) and throw sockaddrs into arrays, let Swift worry about managing their memory, diff those arrays to see what’s changed, and so on, and it all Just Works™.

I see this as one cornerstone of Swift’s “Grand Unified Theory” approach, where (however excited people might get by certain presentations) instead of saying Here Is The One New Way To Do Things, Swift instead allows you to mix and match whatever is appropriate to the task, whether that’s getting all functional, deeply dynamic, protocol-oriented, and more.

Limitations

Swift protocols have one specific limitation: You can’t work with collections of arbitrary heterogenous generic protocols.

You can have collections of specific instances of a generic protocol:

Swift

Specific generic arrays:
// Equatable is a generic protocol: struct Goofy: Equatable { let value: Int } func ==(lhs: Goofy, rhs: Goofy) -> Bool { return lhs.value == rhs.value } // this is fine: let goofy = [Goofy(value: 1), Goofy(value: 2)] // but you can't put a different kind of Equatable in there: let goofy2 = [Goofy(value: 1), Goofy(value: 2), 42] // error!

You can have heterogenous collections of non-generic protocols:

Swift

Heterogenous protocol arrays:
protocol Test { func hello() } struct A: Test { func hello() { print("I am an A") } } class B: Test { func hello() { print("I am a B") } } class C: NSObject, Test { func hello() { print("I am a C") } } extension Int: Test { func hello() { print("I am an Int!") } } let some_tests: [Test] = [A(), B(), C(), 42]

What you can’t do — and this is mostly where people are getting into trouble — is work with collections of arbitrary heterogenous generic protocols. Because it’s typically meaninglessfootnote 1:

Swift

Literally comparing apples to oranges:
struct Apple : Comparable { let value: Int } struct Orange: Comparable { let value: String } func ==(lhs: Apple, rhs: Apple ) -> Bool { return lhs.value == rhs.value } func ==(lhs: Orange, rhs: Orange) -> Bool { return lhs.value == rhs.value } func <(lhs: Apple, rhs: Apple ) -> Bool { return lhs.value < rhs.value } func <(lhs: Orange, rhs: Orange) -> Bool { return lhs.value < rhs.value } // win: let apples = [Apple(value: 23), Apple(value: 17)] let oranges = [Orange(value: "hello"), Orange(value: "world")] // fail: let items: [Comparable] = [Apple(value: 42), Orange(value: "foobar")] // I mean, what would this do? let least = items.minElement()

Now, there is one limitation that doesn’t entirely make sense, at least at first glance, which is that if you have an array of instances of a specific class, you can’t directly cast that to an array of Protocol instances. Using Test/A/B from before:

Swift

Array issue:
// all fine: let one_a: [A] = [A()] let one_b: [B] = [B()] let some_more_tests: [Test] = [A(), B()] // also fine: let one_test: Test = A() // not fine: "error: 'A' is not identical to 'Test'" let yet_more_tests: [Test] = one_a

The reason is due to the way Swift protocols are implemented: when you have an object of protocol type, that is actually a special object instance of about 40 bytes in size.

This is in turn due to that useful fact: that anything, from an Int to a C struct to an Obj-C class can conform to a protocol. You can’t simply cast an array of all those different types, to an array of the protocol, because the underlying data is completely different.

The array has to be prepared for the fact that it could contain Objective-C classes, Swift classes, Swift structs including Ints, Floats, Swift enums, C structs like sockaddrs, kevents, CGPoints and so on — all of which have different sizes and layouts in memory, different semantics (some get copied into the array, some are refcounted) and so on.

So, that’s why it stores these special Protocol instances instead, they’re adapters for the underlying types. And that’s why you can’t just cast arbitrary arrays to them. And why you can’t simply dispatch everything dynamically: some of those things just aren’t dynamic-able, because they might be C structs, Ints, native IEEE floating point values, or whatever, not Objective-C objects.

For single objects (like one_test above), Swift just makes a new Protocol instance. For collections, it doesn’t — perhaps a temporary limitation, perhaps an intentional decision because of the performance implications of creating what could be any number of object instances behind your back. If you want it to make those objects, it will, but you have to be explicit about it:

Swift

Cast, damn you!
let really_tests: [Test] = one_a.map { $0 as Test }

In other words, you can’t cast because it’s not a cast. You have to make a new array with new objects, because they are new objects.

But I Want My Objective-C!

The thing is, Swift has a superset of Objective-C’s protocols, in that you can do everything that Swift protocols support, and also get Objective-C protocols just by tagging a protocol @objc:

Swift

Hello Objective-C:
@objc protocol Test2 { func hello() } class A2: NSObject, Test2 { func hello() { print("I am an A2") } } class B2: NSObject, Test2 { func hello() { print("I am a B2") } } let some_test2s: [Test2] = [A2(), B2()] for test in some_test2s { test.hello() }

Of course, then your objects have to be Objective-C classes, because you’re using the Objective-C runtime, but that’s okay. If you’re saying, “I want Objective-C behaviour”, then you’re saying, “I want Objective-C object instances”.

(Correction: @UINT_MIN writes to say that non-Objective-C classes can conform to @objc protocols. This is correct, of course — they’re on the Swift dev team :) I had a brainfart, I was thinking of a different use of @objc — in Swift 2.0 you cannot declare a class @objc unless it subclasses NSObject. Structs, enums and native types still can’t conform to Objective-C protocols though.)

Because of this, Objective-C protocols in Swift are not magic adapter objects, they’re just the same as in Objective-C (e.g. 4 or 8 byte pointers on 32 or 64-bit systems respectively). They can be cast to, and so in fact, in this specific instance where it’s safe to do so, Swift does let you cast the arrays directly:

Swift

Casting Obj-C Arrays:
let one_a2: [A2] = [A2()] let some_more_test2s: [Test2] = one_a2 for test in some_more_test2s { test.hello() }

Huh. How about that? ;-)

(This also works for Swift classes conforming to Objective-C protocols.)

Purity Schmurity

I believe that this (defaulting to Swift-style protocols, tagging @objc to get Obj-C ones) is the right way round, because the entire Swift runtime library is based on a stack of Swift protocols and their unique properties.

You can’t justify turning Swift protocols into Obj-C protocols, throwing away all of their advantages (not just in speed but also in interoperability) just to avoid typing @objc from time to time.

Let Swift be Swift. Let Objective-C be Objective-C. And always remember that you can use the Objective-C runtime in Swift.

In most casesfootnote 2, when I see people getting snagged on something with protocols, the issue is one of purity.

For some reason, people seem to be of the opinion that Swift code needs to be pure Swift code. I have no idea why. I mean, yes, if you have an array of integers, you probably do in fact want [Int], a pure Swift array of Ints, instead of an NSArray of NSNumbers. And there are many other cases where things like immutable Swift structs and “protocol-oriented programming” are a great choice.

But if what you are doing is fundamentally dynamic in nature, and if the semantics you want are fundamentally dynamic Objective-C semantics… use the Objective-C runtime. That’s what it’s there for!

I don’t know if this stems from some weird ideological thing that people often get into when a new technology comes along, where everything has to be forced into The New Way Of Doing Thingsfootnote 3, or if it stems from some fear that maybe in a couple of years time Objective-C is “going away”, or what?

Regardless of what happens to Objective-C as a language, I find it difficult to believe the Objective-C runtime is going away any time in the next decade, at least, or that even if it eventually does get replaced, it won’t get replaced with something functionally the same or superior — in terms of dynamic runtime features like introspection, dynamic late binding, and so on. Because the entire of Cocoa and Cocoa Touch, Interface Builder and more is built on its foundations, and Apple is continuing to build ever more stuff on top of it, and it’s pretty obviously hugely useful and has been part of their technical DNA since… well, since they were NeXT ;-)

You’ll also note Apple added performSelector: support to Swift recently, and that Swift’s reflection API has moved from undocumented behind-the-scenes implementation detail for Playgrounds and the REPL, to documented part of the language runtime with support for developers to customise its behaviour for their own code.

I think these things show that despite people’s fears, Apple is not backing away from dynamicism, and instead making it easier for people to use the appropriate tool in a given situation. Which is sometimes dynamicism, and sometimes not.

Protocols and structs in Swift are great. When they’re appropriate, use them.

If your code requires inheritance and a class hierarchy, use inheritance and a class hierarchy.

And if it requires dynamic dispatch, KVO, and heterogenous arrays of Obj-C protocols, use them too. It’s okay. The Swift Purity Police are not going to kick down your door.


1. and even when it’s not, the compiler generally doesn’t know that.

2. that aren’t actual compiler bugs, or temporary limitations the Swift team are still working on

3. which, ironically, I believe is not the Swift way of doing things, since I believe the Swift way of doing things is the ecumenical, Grand Unified Theory way. Pragmatism Oriented Programming.