Combining Swift Protocols

In his article Swift Protocols Question, Brent Simmons asks why the following Swift code won’t compile:

protocol Value: Equatable {
}

protocol Smashable {
    func valueBySmashing​OtherValue​(value: Value) -> Value
}

struct Foo: Value, Smashable {
    func valueBySmashing​OtherValue​(value: Value) -> Value {
        return Bar()
    }
}

func == (a: Foo, b: Foo) -> Bool {
    return true
}

struct Bar: Value {
}

func == (a: Bar, b: Bar) -> Bool {
    return true
}

The error message says Protocol ‘Value’ can only be used as a generic constraint because it has Self or associated type requirements. This is a little misleading, because it implies that valueBySmashingOtherValue needs to be rewritten as a generic function, but that actually just pushes the problem down to the adopters of the Value protocol. The real fix is to move the use of the Equatable protocol from the Value protocol to the individual struct declarations, like this:

protocol Value {
}

protocol Smashable {
    func valueBySmashing​OtherValue​(value: Value) -> Value
}

struct Foo: Value, Smashable, Equatable {
    func valueBySmashing​OtherValue​(value: Value) -> Value {
        return Bar()
    }
}

func == (a: Foo, b: Foo) -> Bool {
    return true
}

struct Bar: Value, Equatable {
}

func == (a: Bar, b: Bar) -> Bool {
    return true
}

So why aren’t these two examples equivalent, and why does moving the use of Equatable fix the error?

When you adopt the Equatable protocol, you are promising that two instances of that type can be compared using ==. By adopting the Equatable protocol in the Value protocol, you are promising that any two Value instances can be compared using ==. Since Value is a protocol rather than a class or struct, this is an extraordinary promise to make. You are promising that Foo and Bar and any other types that adopt your Value protocol can be compared, without even knowing if they will be classes or structs or what their implementation details may be.

By moving the Equatable protocol down to the individual struct declarations, we are making the promise much simpler. Instances of Foo can be compared, and instances of Bar can be compared, but there is no promise that a Foo can be compared with a Bar.

But what if you need to compare Foo and Bar objects to each other? In this case you can’t use Equatable, since that’s not what Equatable is designed to do. Because Equatable has a Self requirement, it can only be used to test objects of the same type for equality. To test objects of different types for equality you need to define your own protocol. In this case, it makes sense to make this a part of the Value protocol.

protocol Value {
    func isEqualTo(other: Value) -> Bool
}

protocol Smashable {
    func valueBySmashing​OtherValue​(value: Value) -> Value
}

struct Foo: Value, Smashable, Equatable {
    func valueBySmashing​OtherValue​(value: Value) -> Value {
        return Bar()
    }
    
    func isEqualTo(other: Value) -> Bool {
        if let o = other as? Foo { return self == o }
        return false
    }
}

func == (a: Foo, b: Foo) -> Bool {
    return true
}

struct Bar: Value, Equatable {
    func isEqualTo(other: Value) -> Bool {
        if let o = other as? Bar { return self == o }
        return false
    }
}

func == (a: Bar, b: Bar) -> Bool {
    return true
}

This was covered in the WWDC session Protocol-Oriented Programming in Swift, starting around 37:44 in the video. At 40:05 they also show how to use the new protocol extensions feature in Swift 2 to eliminate the redundant defintions of isEqualTo:

protocol Value {
    func isEqualTo(other: Value) -> Bool
}

extension Value where Self: Equatable {
    func isEqualTo(other: Value) -> Bool {
        if let o = other as? Self { return self == o }
        return false
    }
}

protocol Smashable {
    func valueBySmashing​OtherValue​(value: Value) -> Value
}

struct Foo: Value, Smashable, Equatable {
    func valueBySmashing​OtherValue​(value: Value) -> Value {
        return Bar()
    }
}

func == (a: Foo, b: Foo) -> Bool {
    return true
}

struct Bar: Value, Equatable {
}

func == (a: Bar, b: Bar) -> Bool {
    return true
}

A Protocol-oriented Philosophy

One of the differences between a protocol-oriented language like Swift and an object-oriented language like Objective-C is how they think about code composition. Because most object-oriented languages only allow single inheritance, the class hierarchy is the primary means of sharing code and the tendency is to create deep class trees. In a protocol-0riented language, multiple protocols can be adopted by the same class, and so the tendency is to create shallow hierarchies where classes and structs adopt the particular protocols that apply to them.

Swift 2 takes this strategy even farther by allowing protocols to include code through protocol extensions, which means that the mix-and-match nature of protocols can be used not just to combine interfaces, but implementations as well.

When you are learning to think in Swift, it pays to assume that protocols should be tight and independent, and that structs and classes will adopt as many protocols as apply to them. This limits the interdependence between your modules, while still allowing a large amount of code re-use.