BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Protocol-Oriented Programming in Swift

Protocol-Oriented Programming in Swift

This item in japanese

Bookmarks

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.

Rate this Article

Adoption
Style

BT