At WWDC 2015, Dave Abrahams, of C++/Boost fame and now lead of the Swift Standard Library group at Apple, introduced Swift as a Protocol-oriented language, and showed how protocols can be used to improve your code.
Protocol-oriented programming is an OOP paradigm that prefers the use of protocols (interfaces according to Swift terminology) and structs over classes.
Are classes awesome?
Classes, as they are known in OOP, are used to provide:
- encapsulation
- access control
- abstraction
- namespace
- expressivity
- extensibility.
Actually, Abrahams says, those are all attributes of types, and classes are just one way of implementing a type. Yet, they exact a heavy toll on programmers in that they may cause:
- Implicit sharing, such that if two objects refer a third object, then both can modify it without the other knowing about it. This leads to worarounds such as duplicating the referred object to avoid sharing, which in turn leads to inefficiencies; alternatively, sharing may require using locks to avoid race conditions and this can cause more inefficiency and even lead to deadlocks. What this entails is more complexity, which means more bugs.
- Inheritance issues: in many OOP language, there can be one just superclass, and it has to be chosen at the very start. Changing it later can be extremely hard. A superclass, furthermore, forces any stored property on derived classes and this can make it complex to handle initialization and not to break any invariants that the superclass require. Finally, there are usually limitations to what can be overridden, and how, or when it should not be, and those constraints are usually left to the docs.
- Lost type relationship, which ensues from the conflation of interface and implementation. This usually manifests itself through some base class’ methods where no implementation is possible and thus the necessity to downcast to the concrete derived class in that method’s implementation. This last point is illustrated in the following code snippet:
class Ordered { func precedes(other: Ordered) -> Bool { fatalError("implement me!") } } class Label : Ordered { var text: String = "" ... } class Number : Ordered { var value: Double = 0 override func precedes(other: Ordered) -> Bool { return value < (other as! Number).value } }
According to Abrahams, protocol-oriented programming is a better abstraction mechanism in that it allows:
- value types (besides classes)
- static type releationships (besides dynamic dispatch)
- retroactive modeling
- no forcing of data on models
- no initialization burden
- clarity as to what shall be implemented.
Protocol-Oriented Programming
The first step for a new abstraction in Swift should always be a protocol, Abrahams says. He goes on then to rewrite the Ordered
class example using the protocols and structs approach in order to show how much cleaner the ensuing implementation is:
protocol Ordered { func precedes(other: Self) -> Bool } struct Number : Ordered { var value: Double = 0 func precedes(other: Number) -> Bool { return self.value < other.value } }
In the snippet above, the use of Self
in the precedes
protocol requirement is what makes it possible that the precedes
method implementation in the Number
class correctly gets the proper parameter and no casting is necessary.
The Self
requirement has an important implication when it comes to using a protocol that includes it. In fact, if we define a binarySearch
method that takes an array of Ordered
instances, we could write the following code:
class Ordered { ... } func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int { var lo = 0 var hi = sortedKeys.count while hi > lo { let mid = lo + (hi - lo) / 2 if sortedKeys[mid].precedes(k) { lo = mid + 1 } else { hi = mid } } return lo }
On the other hand, if we use a protocol including the Self
requirement, we need define a generic method:
protocol Ordered { ... } func binarySearch(sortedKeys: [T], forKey k: T) -> Int { ... }
The differences between using Self
requirements and not using them are far reaching. In particular, the Self
requirement puts us the in the static dispatch field and requires the use of generics and homogeneous collections. This is further illustrated in the picture below.
Retroactive modeling
To explore in more detail how protocols and structs can be used to replace a class hierarchy, Abrahams next introduces a Renderer playground aimed at rendering geometrical figures. This sample allows to highlight the possibility of retroactive modeling that protocols and structs provide. In this concrete case, retroactive modeling is applied to create an extension of CGContext
that implements the requirements of a Renderer
protocol:
protocol Renderer { func moveTo(p: CGPoint) func lineTo(p: CGPoint) func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) } extension CGContext : Renderer { ... }
By doing this, the CGContext
type can be used wherever a Renderer
type is used, although CGContext
was defined previously to Renderer
.
On the other hand, it is possible to provide an autonomous implementation of the protocol through a TestRenderer
class that outputs a textual representation of the geometrical figures:
struct TestRenderer : Renderer { func moveTo(p: CGPoint) { print("moveTo(\(p.x), \(p.y))") } func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))") } ... }
The two Renderer
implementation can be used interchangeably.
Protocol extensions
Swift 2.0 introduces a new feature that can make the use of protocols even more convenient: protocol extensions, which is a feature that allows to provide a default implementation for a protocol requirement. This is explained through the following code snippet:
protocol Renderer { func moveTo(p: CGPoint) func lineTo(p: CGPoint) func circleAt(center: CGPoint, radius: CGFloat) func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) } extension Renderer { func circleAt(center: CGPoint, radius: CGFloat) { arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi) } }
In the context of our Renderer
example, having define circleAt
inside a protocol extension makes that implementation shared by CGContext
and TestRenderer
.
Constrained extensions
Protocol extensions allow the specification of constraints on used types. As an example, this allows for the definition of a protocol extension on a CollectionType
only when the element of the collection satisfies a requirement:
extension CollectionType where Generator.Element : Equatable { public func indexOf(element: Generator.Element) -> Index? { for i in self.indices { if self[i] == element { return i } } return nil } }
Declaring Generator.Element
as Equatable
allows to use the ==
operator inside of indexOf
.
The final part of the talk is dedicated to a few more tricks allowed by protocol extensions and constraints, such as beautifying generic function declarations, e.g. going from:
func binarySearch< C : CollectionType where C.Index == RandomAccessIndexType, C.Generator.Element : Ordered >(sortedKeys: C, forKey k: C.Generator.Element) -> Int { ... }
To:
extension CollectionType where Index == RandomAccessIndexType, 2 Generator.Element : Ordered { ... }
When are classes to be used?
To conclude his presentation, Abrahams notes that classes still have their place, in particular if you want implicit sharing, e.g. when:
- Copying or comparing instances does not make sense.
- Instance lifetime is tied to some external effects, as with a TemporaryFile.
- Instances are “sinks” that only modify some external state, such as CGContext.
Furthermore, when using a framework like Cocoa, that is built around the idea of objects and subclassing, Abrahams says, it does not make sense trying to fight against the system. But, when refactoring a large class, using protocol and structs to factor out pieces of it can be much better.