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.
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
}
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.
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.
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.