Swift Associated Types With Default Values

Swift Associated Types With Default Values

I was browsing the Swift forums when I stumbled across a discussion regarding an undocumented feature of Swift (as of this post's date): the ability to give associated types a default value.

I've shown associated types in action before in my article about loadView() - they are a great tool to define generic behaviour inside protocols in the shape of a specific type. As the feature implies, setting a default value for an associated type will allow the children of the protocol to skip having to define a typealias for the type, unless they specifically want to use a different type.

This is the regular way to define a associated type:

protocol Foo {
    associatedtype FooType
}

struct Bar: Foo {
    typealias Foo = BarType
}

With just an equal sign, you can define a default type to it.

protocol Foo {
    associatedtype FooType = BarType
}

struct Bar: Foo {} //Automatically sets FooType to BarType
//unless another type is specified

It is a very simple thing in nature, but I've found this undocumented feature very interesting. Having to set typealiases really bothered me - being forced to provide a typealias means that the type inherting the protocol is certainly going to do something unique, which might not really be true. Consider this structure used to define a HTTP request:

/// The representation of a HTTPClient's request.
public protocol HTTPRequest {
    /// The Value of a HTTPRequest is the response object retrieved after parsing the request's response.
    associatedtype Value
    /// The endpoint path of the request, to be appended after the HTTPClient's baseURL property.
    var path: String { get }
    /// Serializes the response of this request to its associated value type.
    func serialize(data: Data) throws -> Value
}

extension HTTPRequest where Value: Unboxable {
    public func serialize(data: Data) throws -> Value {
        let value: Value = try unbox(data: data)
        return value
    }
}

This structure works very well for my current project because defining path and the Value type is all a request needs to do in order to work, and if Value conforms to Unboxable, the object is even retrieved automatically. This works perfectly for requests such as this one that retrieves an user:

struct UserRequest: HTTPRequest {
    typealias Value = User // is Unboxable
    let path: String = "v1/profiles"
}

But that implementation is not perfect: Using associated types like that means that I can't test an endpoint without defining a fully fledgled response type to it.

Worse: What if I don't need my request to return something meaningful? Perhaps I don't care about the response, perhaps I just need to cache some Data on the device, or a plain dictionary is enough. For all these cases, I have to explicitly set Value to something and code a custom serialize() method.

If that situation was common, I would simply not use associated types for my requests. Luckily, the existence of default values for associated types solve this problem entirely. Now, I can solve it by making all HTTPRequest objects return a Data object by default:

/// The representation of a HTTPClient's request.
public protocol HTTPRequest {
    /// The Value of a HTTPRequest is the response object retrieved after parsing the request's response.
    associatedtype Value = Data
    /// The endpoint path of the request, to be appended after the HTTPClient's baseURL property.
    var path: String { get }
    /// Serializes the response of this request to it's associated value type.
    func serialize(data: Data) throws -> Value
}

extension HTTPRequest where Value: Data {
    public func serialize(data: Data) throws -> Value {
        return data
    }
}

extension HTTPRequest where Value: Unboxable {
    public func serialize(data: Data?, error: Error?) throws -> Value {
        let value: Value = try unbox(data: data)
        return value
    }
}

Plain requests can now work purely with a path property (and return a plain Data object) while still allowing the regular requests to provide their custom responses.

struct ABTestDataRequest: HTTPRequest {
    let path: String = "v1/abtest"
}

Follow me on my Twitter - @rockbruno_, and let me know of any suggestions and corrections you want to share.

Reference

SR-8761