The subtleties of protocol extensions

Tjeerd in 't Veen

Tjeerd in 't Veen

— 8 min read

One of those things that are not immediately obvious in Swift is that we can constrain an extension with a protocol in several ways; Such as constraining an array with a protocol, or when using protocols as types. In this article we’ll cover the differences.

Modeling data with a protocol

In the following code, we want to model the data-layer of a hot cryptocurrency startup application that wants to use the blockchain to apply machine learning and AI for internet of things (IoT) devices.

Using Swift on the backend, this app works with multiple types of cryptocurrencies which we’ll model in this article. We could consider putting all the types of currencies inside a single enum, but since this list can be enormous, a protocol is a better choice.

First, we model the protocol which we aptly name CryptoCurrency. We use NSDecimalNumber for handling currency. The name represents the coin, and the amount property represents how many—or a fraction—we have of a cryptocurrency.

protocol CryptoCurrency {
    var name: String { get }
    var price: NSDecimalNumber { get }
    var amount: Decimal { get }
}

We can, for example, have DogeCoin and BitCoin types that conform to the CryptoCurrency protocol.

struct Bitcoin: CryptoCurrency {
    let name = "Bitcoin"
    let price: NSDecimalNumber
    let amount: Decimal
}

struct DogeCoin: CryptoCurrency {
    let name = "DogeCoin"
    let price: NSDecimalNumber
    let amount: Decimal
}

Creating an extension

Some time passes, and we have our application up and running. We notice that we make use of repeated actions, such as figuring out the total price of the coins that we store inside arrays. For this, we can offer a convenient extension on Array in which we offer a computed property called totalPrice. This property is of type NSDecimalNumber since we’re still dealing with currency.

When we have an array of coins, the totalPrice property becomes instantly available. Below we’ll add a DogeCoin and BitCoin to an array, and we read out the totalPrice, formatted by a NumberFormatter.

// We create an array of different coin types.
let coins: [CryptoCurrency] = [
    DogeCoin(price: 0.002150, amount: 300),
    Bitcoin(price: 3661, amount: 1)
]

let formatter = NumberFormatter()
formatter.minimumFractionDigits = 2
// We use the totalPrice property here
formatter.string(from: coins.totalPrice) // 3661,64

Note how we made the coins array of type [CryptoCurrency], since this array is heterogeneous.

Putting totalPrice available as an extension makes much sense semantically, it saves us from coming up with a whole new structure or function to calculate our total price.

To implement the totalPrice computed property, we start extending Array. A first approach that we can take is to constrain Array’s Element to CryptoCurrency, by doing so we know that inside the extension, Element is of type CryptoCurreny; This allows us to use the properties of CryptoCurrency.

Then, inside this extension, we’ll create a totalValue computed property which loops through all coins while simultaneously building up the total price.

However, we’ll run into a shortcoming in our approach which we’ll fix shortly.

// We extend array and constrain its Element to CryptoCurrency.
extension Array where Element: CryptoCurrency {
    // We declare a totalPrice property
    var totalPrice: NSDecimalNumber {
        // We declare a temporary value called total
        var total: NSDecimalNumber = 0
        // For each coin...
        for coin in self {
            // ... we add the value to the total sum.
            // (Note how NSDecimalNumber returns a new number)
            total = total
                .adding(coin.price)
                .multiplying(by: NSDecimalNumber(decimal: coin.amount))
        }

        // In the end we return the sum.
        return total
    }
}

This extension is legit, however, it doesn’t work for heterogeneous arrays like ours. It works if all coins are of the same type. Let’s have a look.

First, we call totalPrice on an array of Bitcoin types which works just fine with our extension.

// We put multiple of the same type in an array.
let bitcoins = [
    Bitcoin(price: 3661, amount: 1),
    Bitcoin(price: 3661, amount: 1)
]

// The totalPrice property is now available to us.
formatter.string(from: bitcoins.totalPrice) // 7322

But, having the same coins repeated inside an array doesn’t work for our use-case, we have different coins that we want to mix in our array. If we try to mix currencies, we get an error when calling totalPrice.

let mixedCoins: [CryptoCurrency] = [
    DogeCoin(price: 0.002150, amount: 300),
    Bitcoin(price: 3661, amount: 1)
]

formatter.string(from: mixedCoins.totalPrice) // Doesn't work (yet)

The error that the compiler throws is

error: using ’CryptoCurrency’ as a concrete type conforming to protocol ’CryptoCurrency’ is not supported

What the compiler is telling us, is that when we have an array of [CryptoCurrency] we’re using a protocol as a concrete type. However, in our extension, we used the protocols as a constraint. This is a mismatch that we’ll solve next.

Fixing our extension

To obtain the totalPrice extension on a heterogeneous array, we have to create the extension a bit differently. We’ll use the == instead of : when defining our where clause.

// We had this extension for concrete types.
extension Array where Element: CryptoCurrency { ... }

// We'll create an extension for using a protocol as a concrete type.
extension Array where Element == CryptoCurrency { ... }

With the : where clause, we offer an extension for elements that conform to CryptoCurrency, e.g. Bitcoin or DogeCoin.

With the == where clause, we offer an extension for elements where the CryptoCurrency is used as a type, e.g. an array of type [CryptoCurrency].

Below you can find the full extension that supports heterogeneous arrays.

extension Array where Element == CryptoCurrency {
    var totalPrice: NSDecimalNumber {
        var total: NSDecimalNumber = 0
        for coin in self {
            total = total
                .adding(coin.price)
                .multiplying(by: NSDecimalNumber(decimal: coin.amount))
        }

        return total
    }
}

Everything works, and now we can call totalPrice on heterogeneous arrays.

let mixedCoins: [CryptoCurrency] = [
    DogeCoin(price: 0.002150, amount: 300),
    Bitcoin(price: 3661, amount: 1)
]

formatter.string(from: mixedCoins.totalPrice) // 7322

Become a Swift pro

Updated for Swift 5!

Check out Swift in Depth and master tough concepts; Such as Generics, Functional Programming concepts, and Protocol-Oriented Programming.

Conclusion

It’s striking how one small symbol makes a world of difference. To better understand extensions, we have to understand using protocols as types versus using protocols as constraints. Not to mention that we have to keep an eye out for the symbol == versus : symbol. Note that you can offer both extensions side-by-side to offer extensions for multiple scenarios.

If you like to know more about using protocols as types, I’d like to refer you to the Reasoning about protocols article where we handle protocols in a bit more detail. Alternatively, you can check out the Swift in Depth book for a deep dive into protocols.


Written by

Tjeerd in 't Veen has a background in product development inside startups, agencies, and enterprises. His roles included being a staff engineer at Twitter 1.0 and iOS Tech Lead at ING Bank.