Swift Tip: Protocols vs. Values
Last week, we compared enums and protocols in terms of extensibility. This week we'll explore how regular values can be used instead of a protocol, and how the two approaches differ in terms of extensibility. We learn that a type can conform to a protocol at most once, whereas you can have many "instances" of regular values.
In the tiny networking library we built for Swift Talk, we use a simple struct to describe a resource that can be loaded from the network:
import Foundation
struct Resource<A> {
var request: URLRequest
var parse: (Data) throws -> A
}
This struct contains all the information needed to make the request (in the URLRequest
value), as well as a function that knows how to turn the data from the network into an A
, whatever that type might be.
An alternative approach would be to define a protocol to which types loadable from the network can conform:
protocol Loadable {
static var request: URLRequest { get }
init(parsing data: Data) throws
}
This protocol defines the same requirements as the Resource
struct above, just in a slightly different form: a static property for the URLRequest
, and a throwing initializer to turn data into an instance of conforming types.
Let's say we want to load a list of countries from a webserver with either one of these approaches. First, we need a Country
type:
struct Country: Codable {
var alpha2Code: String
var name: String
var population: Int
}
Since Country
conforms to Codable
, we can use the JSONDecoder
to turn the incoming data into Country
values. To avoid writing this boilerplate code for each JSON endpoint, we can define an initializer on the Resource
struct for types that are Decodable
:
extension Resource where A: Decodable {
init(get url: URL) {
self.init(request: URLRequest(url: url)) { data in
try JSONDecoder().decode(A.self, from: data)
}
}
}
For the protocol approach, we can add the same convenience with a default implementation of the init(parsing:)
initializer in case the type conforms to Decodable
:
extension Loadable where Self: Decodable {
init(parsing data: Data) throws {
self = try JSONDecoder().decode(Self.self, from: data)
}
}
To load the countries using the Resource
approach, we first have to create a resource describing the endpoint. Then we use a custom load
method on URLSession
to load the data (for the full code see this gist).
let countries = Resource<[Country]>(get: URL(string: "https://restcountries.eu/rest/v2/all")!)
URLSession.shared.load(countries) { print($0) }
To do the same with the protocol based approach, we have to conform Array
to Loadable
if its elements are of type Country
:
extension Array: Loadable where Element == Country {
static let request = URLRequest(url: URL(string: "https://restcountries.eu/rest/v2/all")!)
}
URLSession.shared.load([Country].self) { print($0) }
Unfortunately, here we run into a limitation of protocols: each type can conform to a protocol at most once. For example, we can't conform Array
again for a different type of element. We might try to conform Array
when its elements are Loadable
:
extension Array: Loadable where Element: Loadable {
static let request = ;func...?()
}
However, since Loadable
types need to specify the URL they can be loaded from, this approach doesn't work either. We can't specify a URL for a generic array of Loadable
elements.
Being able to conform to protocols only once is also a problem for protocols such as Codable
. For example, if Apple ever provides conformance for CLLocationCoordinate2D
, it has to pick a single representation. In the API we've used above, a location coordinate is represented as an array of numbers, but we have also used APIs where it's represented as {lat: 39, lon: 22}
or {latitude: 39, longitude: 22}
. JSONDecoder
solves this problem by providing options for common variations, like date formats. However, if the decoder doesn't have support for a format you need, we have to resort to using a wrapper type, as we've discussed in a previous post.
When designing your own APIs, think twice about whether it would make sense to conform a type multiple times. If yes, try using values like the Resource
struct rather than protocols.
You can read much more about protocols in our book, Advanced Swift. A newly expanded edition is almost ready for release, and the update will be free for everyone who already owns the Ebook. 👍