What the 55 Swift Standard Library Protocols Taught Me
How does the way the Swift team uses protocols, give us hints on how we can do it?
Alright. 55 (with an asterisk) public protocols in the Swift Standard Library; 18 minutes on the clock; let's get started.
I just want to start by asking: Why protocols? And why protocol-oriented programming? If we go back to the innocent salad days of object-oriented programming, a lot of us are coming from Objective-C, which means we were free from the tyranny of multiple inheritance. Or if you're from the other half of the room that likes C++, then we were denied the enlightenment of multiple inheritance, and we can argue about that later on tonight.
With single inheritance class hierarchies things are linear: you've got parents, child, grandchild all the way down the family tree. And when you go back up the tree, then everything has a single parent. That keeps the hierarchy clean. But then you do lose the benefits that multiple inheritance can offer when used properly, and in Swift there's no inheritance for enumerations and struct types- it's class types only. That means you sometimes need to twist yourself into a pretzel to make sense of your types. You can end up with really generic super classes. And then many, many levels down, if you can imagine more levels in between here, before you get to a leaf node in your graph, and you have a class that you can actually instantiate and use.
You sometimes need to twist yourself into a pretzel to make sense of your types
So with protocols, you can make the type system a little bit more compositional and you can clean up the long chain of inheritance. Of course you're going to be trading a long tall chain of inheritance, for a wide chain of protocol conformance. But I think the tradeoffs are worth it, and I hope you'll be convinced by the end of this talk.
What kind of things make sense to put into protocols? I'm not going to talk about cool protocols that I've made. But instead I want to take a tour of the protocols that Apple has shipped in the Swift standard library. We'll take a little tour, maybe you'll learn about some protocols that you haven't heard of before. We'll see what big ideas that we can take, and then maybe get some inspiration on what kinds of things we can use with protocols in our own code. Basically the idea of this talk is how does the way the Swift team uses protocols, give us hints on how we can do it?
Ultimately the goal is for you to start thinking of protocols, come up with some cool ideas and then open source them, and then I will star your repository on GitHub.
Three categories of protocols: "Can do", "Is a" and "Can be"
The Swift standard library includes 54 actual public protocols. My initial jokey pitch for this talk, was I would go through them one by one, taking 16 seconds each to fill the time, but instead, I've broken them up into three categories. We've got the “Can do” protocols the “Is a” protocols and then the “Can be” protocols. So we'll take them one at a time, see some examples and then see what lessons that we can take.
“Can do” protocols
First up we got the "Can do" protocols. And these describe things that the type can do or have done to it. And they also end in the -able syntax which makes them easy to spot as you're browsing through the headers. Here's a first example: A type that conforms to the Hashable protocol, means that you can hash the thing down to an int. That means that you can store it in a set, it can be the key to a dictionary and so on. You've also got the Equatable and the Comparable protocol and that means that you can compare two instances of some value against each other with the various equality and comparison operators available in Swift. These are pretty common protocols that you might have implemented yourself in your own types. And you'll notice that these protocols describe operations that you can perform on the type. There's comparison, equality, hashing.
AbsoluteValuable. It sounds so important. Just because it has that word "valuable" at the end of it
As a side trivia note, let's talk about what I think is the best named protocol that deserves a special transition: AbsoluteValuable. It sounds so important. Just because it has that word "valuable" at the end of it, but unfortunately it just sounds more important than it is. All that means is that it supports the absolute value function.
There's also a small sub set of protocols in this "can do" group that have to do with alternate views or alternate representations.
Let's have a look at a quick example and that's RawRepresentable. So that means that the type can be represented as some kind of raw value and then you can turn that raw value back into the value that's the actual instance. This should sound an awful lot like enumerations in Swift, where you have raw values built in. So all that functionality of enumerations and that's having an initializer, that takes a raw value, and then once you have an enumeration value getting the raw value version of it. All that stuff is built into this protocol here. So you can do something similar with your own structs and class types as well, if you like. The idea here is that the intrinsic value of the thing is the same, you're just changing the outside representation of it. But, there's a one-to-one mapping between the raw value, and the instance version of the value.
Next up is CustomPlaygroundQuickLookable in this category. And that just means that your type can be quick looked from a playground. Then that means that again, your type is the same, you're not really converting it to something else but you're providing an alternate view for your value. In this case it's something that you can display in a quick look.
So we've got Operations, we've got Alternate Views. What's the lesson that we can take?
If you had an operation on your own types, say you were writing the next Instagram Killer photo filtering App, then you could add something like a filterable protocol that you can then have your photo instances, your images, conform to. Then say, your filtering app becomes a hit, it really takes off, you wanna expand to videos too. And videos are just another form of media. You could in theory also apply your filterable protocol to videos, audio, 3D photos, whatever happens to come up in the future.
And what about an example of Alternate Views? There's always creating thumbnails from large photos, which you can think of as an alternate view for the full-sized photo. Again, this isn't actually a conversion, it's just an alternate representation. So, you could imagine something like a thumbnail-able protocol, hopefully you'll come up with a better name, and maybe the audio version of a thumbnail. A thumbnail is like a low bit rate version of the audio, or something like that.
The basic idea here is to take these common operations that are in your app and in your code, and protocolize them, if that's a word. Why would you want to do this? One benefit is to make the concept reusable. You have several types that have some common operation that you need to apply, and now they can share a guaranteed common interface. You can get the benefits of polymorphism, even in your structs and your enumerations. Also, having this kind of compositional approach helps you separate the thing from it's operations. I'm sure opinions can go either way here, but I like the idea of building up a type from smaller pieces like this, based on what they can do. So, that's the first category of protocols, Operations, Alternate Views. You're building up your set of operations and views that will apply to your type.
“Is a” protocols
Next step are the “Is a” protocols, and these describe what kind of thing the type is. In contrast to the "can do" protocols, these are more based on identity. That means conforming to multiple protocols of this kind feels the closest to something like multiple inheritance in Swift. You can identify these protocols in the standard library because they end in the word “-type". And this is fully half of the standard library, something like 35 or so of those 54 are of this kind. Let's look at an example. CollectionType is a good one. Not surprisingly Array, Dictionary, and Set conform to CollectionType. Maybe slightly more surprisingly, things like Ranges and String views. If you have a string, you can ask it for UTF-8 representation or a UTF-16 representation of the string. So, it's just a series of Unicode code points. And so, that also conforms to CollectionType.
Then there are protocols for covering some of the primitives like IntegerType, FloatingPointType, BooleanType and so on. With these ones you can think of the protocols more like groupings. So there are several integer types. We've got unsigned int, signed int, 16-bit int and so on. But they're all grouped together, because they conform to the common integer type protocol. If you come up with your own integer type, maybe you want a 4-bit integer or 6-bit integer, to be a little contrarian, then why not have it conformed to the protocol. But, that's unlikely. With most of these type protocols you'll probably never write your own type to conform to these. If you look at the header comments for the BooleanType for example, that actually discourages you from creating more boolean types, because one is probably enough. Another example is the MirrorPathType, which has this delightful comment next to it in the headers: "Do not declare new conformances to this protocol, they will not work as expected."
So, as you can see, that means that many of the protocols in this category are ones where you might use the conforming types. I'm sure everybody has used an array or an integer but you probably won't create your own types to conform to them. There are a few that you might use though- We've got ErrorType, which we heard about earlier from Thomas, to use with the new error handling model in Swift 2. There is SequenceType and GeneratorType, if you're building something that's Collection-ish and you want to support iteration, you can have a look at these protocols.
So that's the “Is a” protocols. Protocols as identity. And what are the lessons that we can take from this pattern of protocol? Again, since these are identity based rather than operation based, you can use them for your larger groupings of your types. Back to the canonical animal kingdom example: Here's an exaggerated class hierarchy that's very long. We don't even have a type at the bottom than we can instantiate. Each step in this class hierarchy adds a little bit of functionality on top of the previous one. So, with protocols you can make your type system more compositional. You have this menu of protocols that you can build and take to use in your different types. Barks and meows for example, are more of a "can do" style of what the animal can do, whereas 'two legs' and 'four legs' are more of an identity type. And you'll notice that two legs and four legs also has inheritance, because protocols can have inheritance like that.
That means, once you have these protocols set up, you can build up what used to be that giant list of super-classes and you instead have a set of protocols, with inheritance if you need it. Then when you construct your type, you can choose which slices of identity here and functionality apply to the type. Since your types can conform to multiple protocols, this is how you build up the functionality of your type little by little based on protocol conformance.
That's the second type of standard library protocol- The “Is a” protocols, having to do with grouping things together and identity.
"Can be" protocols (11.28)
Finally we have the “Can be" types. Rather than just an alternate view of the same thing, as we have already seen, these are more about straight on conversion. Changing from type X over to type Y. And these ones end in the word “-Convertible". So that means that the type can either be converted from or converted to something else.
Lets take a look at a couple of examples. We’ve got the simple initializer style ones, such as FloatLiteralConvertible, IntegerLiteralConvertible, ArrayLiteralConvertible, and so on. If your type conforms to FloatLiteralConvertible then that means you need to have an initializer that takes some kind of a floatLiteral, that's a double by default, and then it builds your type. So the conversion is going in that direction from a float over to your type.
In contrast there are protocols like everybody's best friend CustomStringConvertible, or the protocol formerly known as Printable. Which specifies that your type can be converted into a string, so the conversion is going in the other direction- You are going from your type and turning it into a string.
An Objective-C practitioner sees that and says- 'Ah that's nothing. I type method signatures longer than that every time I code.'
As another trivia side note, here's the protocol with the longest name among the 54, and it's in this category, ExtendedGraphemeClusterLiteralConvertible - 41 characters. I'm sure those of you coming from Objective-C are saying, or laughing, and like, "Ah that's nothing. I type method signatures longer than that every time I code." So that's Protocols for Convertibility in the final group. What are some of the lessons we can take from this kind of protocol, other than trying to keep the names of your types short?
This one is pretty straight forward. If you have types that can become other types, then don't just add a method, or add a computed property, or add an initializer. Think about setting it up as a protocol. Remember you can use the protocol to either specify conversion to or from your type. The other required example for any technical talk, aside from the animal one, is the employee database. If you have objects for people, regular employees, managers, contractors, then each of those kinds of people might be a separate type. If a contractor can get hired as an employee, or an employee can be promoted to a manager, then that's a kind of a conversion. You don't want to reenter the persons name, address, phone number and social security number, and all of that. You want to change a contractor into an employee relatively seamlessly. For that you could have something like an EmployeeConvertible protocol. And then say the contractor type and the interviewee type could conform to it.
Whats the benefit of this though? Why even have a protocol, plus a conversion method rather than just a method, which seems simpler? Again part of it is the compositional approach. The fact that an interviewee can become an employee is part of what the type is, but it's not exclusive. Other kinds of people can become employees as well. By using a protocol you can assure that there's a common well defined interface for converting some kind of person into an employee.
There's also a nice code as documentation benefit here. If you're browsing through your code, or someone else's code in the project and you see 'EmployeeConvertible' and you're already familiar with it, that tells you something right there about what the type can do and what some of the interface is like. Then you can also grep through your project and look for the word 'EmployeeConvertible', and then right there in the search results, that's the list of types that can become employees. So that's the “Can be" protocol family, handling conversions between your types.
Four broad patterns
So we've seen three kinds of protocols from the standard library having to do with capability, identity, and conversion. What are the broad patterns that we can think about that we've seen from our own code? We've got four of them:
- Operations- if there are a common set of operations that you have to perform on your types, consider breaking these out into a protocol.
- Related to that was Alternate views- if your type has an alternate view, or an alternate representation that's not quite a full on conversion, think about whether it belongs as a common protocol.
- For identity- this is your chance to have something like multiple inheritance, or mix-ins in your types. Thinking about identity and what the types are and grouping similar types together with protocols.
- Finally we have conversions, whether you convert from a type, or to a type, if that particular conversion is happening a lot in your code consider breaking that very common conversion out, as a protocol to help you keep track of things, and to keep the interface consistent.
I think seeing how much Apple has done to move common functionality, things like map, filter, the enumeration methods, into protocols using just plain old protocols and also protocol extensions, is a really great example and an inspiration on how powerful the future can be. Apple is following this example. If you look at the definition of array, for example, it conforms to eight protocols, string conforms to 12 protocols, and so on. So the idea here is that you're creating these feature bundles in protocols, and then you can just use them from all over your code base.
I think thinking about your types in this way can help you keep things straight and categorized in your head. So I definitely encourage you to try this out in your own code. Look at your types through the lens of what they have in common using protocols.
And that's all I’ve got. Protocols forever, thank you!
Q1: Hi, Dave Ungar again. So this left me wondering and, my own coding leaves me wondering when not to do a protocol. For example I have a struct, it has 10 functions. Maybe I should do 10 protocols. And the particular example from the standard library would be, there's no string protocol. Yet I've had to invent one in order to put a Where clause to something else that was generic where the type parameter could be a string. So could you say either when not to use protocols or why there's no string protocol when I needed one? Thanks.
Greg: That’s Apple, right? Never providing for everybody's needs. But I think for when to use or when not to use them I think the sort of measure I'd been using is again just how common this is. And if I'm only gonna have one type that conforms to the protocol, then it's pretty much, I'll just leave it to the type. If the type does 10 things then so be it. But if it happens even twice, just more than once, that's enough for me to say, "Okay, it's common enough to happen twice" which seems like a miracle to have some kind of duplication, and maybe that'll happen in more than one time. So it really is a judgment call. But I would say, more than once is a requirement at least.
Audience: God. So it's worth duplicating the functions, signatures and all of that stuff to get the protocol.
Greg: Right. Yeah.
Q2: Hello. I was wondering, have you ever had to deal with generics in protocols? And if so, what was your strategy?
Greg: I had an example of that I think, because you can't have generics attached to the protocols but you can do the type alias which you see a lot, you just define type alias. You can call it type alias T if you want. If you want it to seem like generics, and then when you define your type you just have to define the type alias as a concrete type. So that's the method that I've seen to do them in protocols.
Audience: I think that there are cases where it becomes a little difficult, and for example if you were to declare a variable, and you were to say, "I want this variable to conform to this protocol." If that protocol has a type alias in it, you can't do that because the compiler doesn't like it.
Greg: Yes, it’s more like if you're defining your own type and you can give the type alias then that's an option too. But I haven't found really a better universal solution for the variable situation than that.
Q3: Thank you very much for this talk, I really enjoyed it. Thinking about protocols this way seems to make it much clearer for me. My question is, you said there were four times where you can use protocols, or think about it, Operations, Alternate Views, Identity and Conversions. My question is, can you clarify a little bit more about the Alternate Views, the differences between Alternate Views and Convertibles. The way I typically think about it is, at times I can convert a type into an alternate view, but you seem to make it different somehow, right?
Greg: Yeah, I get the question. Part of the problem is just the way the protocols are named. Maybe they're poorly named. Again, Printable used to be an -able protocol and now it's CustomStringConvertible. So it's switched sides so to speak, right? But I think the way that I've looked at it is that an alternate view means, the thing itself isn't changing, it's just what it looks like from the outside is changing. A full size image to a thumbnail, or a enumeration value to a raw value. It's really the same thing, it's just you're looking at it from a different lens. Whereas the convertible protocols are like, "I have an integer and I'm changing it to a string." It's CustomStringConvertible, right? You're changing it to an entirely different type, it's not just a different view with its own thing and it's gonna have its own life cycle down the road. That's the way that I've looked at it.