Blog

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


  • See the Swift Talk Collection


Stay up-to-date with our newsletter or follow us on Twitter .

Back to the Blog

Recent Posts