Logical Protocols - Making Defaults Work

Posted on February 4, 2018

Every software engineer likes free code. The less code you write, the easier it is to reason about and test your code. This post will talk about yet one more way to make more of your code reusable. Let’s consider the example of ordering a list in Swift. For the sake of clarity, let us assume we are implementing a standard library, with no existing code or libraries. We really don’t care what’s in the list, but rather that what’s in the list is Orderable. To capture this behavior we can create the following protocol:

enum Order {
  case Greater
  case Equal
  case Less
}
protocol Orderable {
  func order(comparing: Self) -> Order
}

This enables us to write a new function on array for sorting elements in the list. Once the celebration has ended, we are now tasked with implementing a new function to find all members which are equal to a specified value allEqual(to: Self) -> [Self]. You may notice that the protocol Orderable can give us this information, we can just filter the list for those members which return Equal. However, this protocol also offers additional logic for determining if the elements are greater or less than. Requiring this additional logic for determining equality would be cumbersome, so we define a new protocol:

protocol Equatable {
  func equals(other: Self) -> Bool
}

This protocol allows us to only have knowledge of equality and still get our wonderful new allEquals function. However, now all the Orderable kinds must also implement this functionality (presumably to return order(:) == .Equal). We might be tempted to implement Equatable for the Orderable type:

extension Orderable: Equatable {
  func equals(other: Self) -> Bool {
    return self.order(comparing: other) == .Equal
  }
}

However, this is a compiler error: Protocol Orderable cannot have an inheritance clause. However, we don’t want to have to reimplement Equatable for every Orderable instance… What we can do is define a new protocol in terms of the other two! Consider the following Orderable_Equatable protocol:

protocol Orderable_Equatable: Orderable, Equatable {
}

extension Orderable_Equatable {
   func equals(other: Self) -> Bool {
    return self.order(comparing: other) == .Equal
  }
}

What this does is create a protocol which conforms to both Orderable and Equatable, but only provides the Equatable implementation. Now, for any type conforming to orderable, we can padd equality with this one liner:

extension MyConcreteOrderableType: Orderable_Equatable {}

Applying this protocol to a type which doesn’t implement Orderable will result in a compiler error because there is no instance for order(comparing:). This allows you to share the implementation without adding the implementation to an inheritance tree, preserving composibility.

Even though this is helpful, it’s rather unsightly. We can do better. Instead of creating a new protocol, we can extend the existing protocol with Swifts conditional Conformance. We can define an extension to Equatable which provides an implementation for Ordered instances:

extension Equatable where Self: Ordered {
  func equals(other: Self) -> Bool {
    return self.order(comparing: other) == .Equal
  }
}
extension MyOrderedType: Equatable {}

Now we can just add the protocol we want, without having to remember which protocol provides the default implementation. If a default exists, we get it for free.

However, this still has problems. If there are multiple default implementations Swift will require that you provide an implementation for the protocol. Otherwise the implementation is ambiguous.