Protocol Extensions

Jon Hoffman

February 2016

In this article by John Hoffman, the author of Protocol Oriented Programming with Swift, you will study the types of protocols that can be extended. Protocol extensions can be used to provide common functionality to all the types that conform to a particular protocol. This gives us the ability to add functionality to any type that conforms to a protocol, rather than adding the functionality to each individual type or though a global function. Protocol extensions, like regular extensions, also give us the ability to add functionality to types that we do not have the source code for.

(For more resources related to this topic, see here.)

Protocol-Oriented programming would not be possible without protocol extensions. Without protocol extensions, if we wanted to add specific functionality to a group of types that conformed to a protocol, we would have to add the functionality to each of the types. If we were using reference types (classes), we could create a class hierarchy, but this is not possible for value types. Apple has stated that we should prefer value types to reference types, and with protocol-extensions, we have the ability to add common functionality to a group of value and/or reference types that conform to a specific protocol without having to implement that functionality in all the types.

Let's take a look at what protocol extensions can do for us. The Swift standard library provides a protocol named CollectionType (documentation here). This protocol inherits from the Indexable and SequenceType protocols and is adopted by all of Swift's standard collection types such as Dictionary and Array.

Let's say that we want to add the functionality to types that conform to CollectionType. These would shuffle the items in a collection or return only the items whose index number is an even number. We could very easily add this functionality by extending the CollectionType protocol, as shown in the following code:

extension CollectionType {
    func evenElements() -> [Generator.Element] {

        var index = self.startIndex
        var result: [Generator.Element] = []
        var i = 0
        repeat {
            if i % 2 == 0 {
                result.append(self[index])
            }
            index = index.successor()
            i++
        } while (index != self.endIndex)
        return result
    }

    func shuffle() -> [Self.Generator.Element] {
        return sort(){ left, right in
             return arc4random() < arc4random()
        }
     }
}

Notice that when we extend a protocol, we use the same syntax and format that we use when we extend other types. We use the extension keyword, followed by the name of the protocol that we extend. We then put the functionality that we add to the protocol between curly brackets. Now, every type that conforms to the CollectionType protocol will receive both the evenElements() and shuffle() functions. The following code shows how we can use these functions with an array:

var origArray = [1,2,3,4,5,6,7,8,9,10]

var newArray = origArray.evenElements()
var ranArray = origArray.shuffle()

In the previous code, the newArray array would contain the elements 1, 3, 5, 7, and 9 because these elements have even index numbers (we are looking at the index number, not the value of the element). The ranArray array would contain the same elements as origArray, but the order will be shuffled.

Protocol extensions are great to add functionality to a group of types without the need to add the code to each of the individual types; however, it is important to know what types conform to the protocol we extend. In the previous example, we extended the CollectionType protocol by adding the evenElements() and shuffle() methods to all the types that conform to the protocol. One of the types that conform to this protocol is the Dictionary type; however, the Dictionary type is an unordered collection. Therefore, the evenElements() method will not work as expected. The following example illustrates this:

var origDict = [1:"One",2:"Two",3:"Three",4:"Four"]
var returnElements = origDict.evenElements()
for item in returnElements {
    print(item)
}

Since the Dictionary type does not promise to store the items in the dictionary in any particular order, any of the two items could be printed to the screen in this example. The following shows one possible output from this code:

(2, "two")
(1, "One")

Another problem is that anyone who is not familiar with how the evenElements() method is implemented may expect the returnElements instance to be a dictionary. This is because the original collection is a dictionary type; however, it is actually an instance of the Array type. This can cause some confusion; therefore, we need to be carful when we extend a protocol to make sure that the functionality we add works as expected for the types that conform to the protocol. In the case of the shuffle() and evenElements() methods, we may have been better served if we added the functionality as an extension directly to the Array type, rather than the CollectionType protocol; however, there is another way. We can add constraints to our extension that will limit the types that receive the functionality defined in an extension.

In order for a type to receive the functionality defined in a protocol extension, it must satisfy all the constraints defined within the protocol extension. A constraint is added after the name of the protocol that we extend using the where keyword. The following code shows how we could add a constraint to our CollectionType extension:

extension CollectionType where Self: ArrayLiteralConvertible {
  //Extension code here
}

In the CollectionType protocol extensions, as shown in the previous example, only types that also conform to the ArrayLiteralConvertible protocol will receive the functionality defined in the extension. Since the Dictionary type does not conform to the ArrayLiteralConvertible protocol, it will not receive the functionality defined within the protocol.

We could also use constraints to define that our CollectionType protocol extensions only apply to a collection whose elements conform to a specific protocol. In the next example, we use constraints to make sure that the elements in the collection conform to the Comparable protocol. This may be necessary if the functionality that we add relies on the ability to compare two or more elements in the collection. We could add the constraint like this:

extension CollectionType where Generator.Element: Comparable {
    // Add functionality here
}

Constraints give us the ability to limit which types receive the functionality defined in the extension. One thing that we need to be careful of is using protocol extensions when we should actually be extending an individual type. Protocol extensions should be used when we want to add a functionality to a group of types. If we try to add the functionality to a single type, we should look at extending this individual type.

We created a series of protocols that defined the Tae Kwon Do testing areas. Let's take a look at how we can extend the TKDRank protocol from this example to add the ability to store which testing areas the student passed and the areas in which they failed. The following code is for the original TKDRank protocol:

protocol TKDRank {
    var color: TKDBeltColors {get}
    var rank: TKDColorRank {get}
}

We will begin by adding an instance of the Dictionary type to our protocol. This dictionary type will store the results of our tests. The following example shows what the new TKDRank protocol will look like:

protocol TKDRank {
    var color: TKDBeltColors {get}
    var rank: TKDColorRank {get}
    var passFailTests: [String:Bool] {get set}
}

We can now extend the TKDRank protocol to add a method that we can use to set in instances where the student passes or fails individual tests. The following code shows how we can do this:

extension TKDRank {
    mutating func setPassFail(testName: String, pass: Bool) {
    passFailTests[testName] = pass 
}

Now, any type that conforms to the TKDRank protocol will have the setPassFail() method automatically.

Since we have seen how to use extensions and protocol extensions, let's take a look at a real-world example. In this example, we will explore ways in which we can create a text validation framework.

Summary

In this article, we looked at an extension. In the original version of Swift, we were able to use extensions to extend structures, classes, and enumerations, but starting with Swift 2, we are able to use extensions extend protocols as well.

Without protocol extensions, protocol-oriented programming would not be possible, but we need to make sure that we use protocol extensions where appropriate, and do not try to use them in place of regular extensions.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Protocol-Oriented Programming with Swift

Explore Title