Cocoaphony

ASCII

Every part of this post is over-simplified. History is messy and difficult to sum up. The original version was twice as long and still over-simplified. Just go with it.

ASCII chart with 8 sticks (i.e. columns) and 16 rows, demonstrating the underlying structure of ASCII
Public Domain. Source: WikiCommons

In 1961 the American Standards Association began developing a new character encoding to replace the dozens of existing systems. In the end they created the 7-bit system that we call the American Standard Code for Information Interchange. We take ASCII for granted today, almost obvious, but it’s actually very carefully designed and surprisingly clever. ASCII is inspired by automated telegraph codes that evolved to drive teletypes.

AnyCodingKey

Let’s talk about CodingKey. It’s a protocol. It is not a magic enum thing. Coding keys do not have to be enums. There is some special compiler magic for when CodingKeys are enums, but it’s just a protocol.

It’s something that wraps a string value, that may also wrap an int value. That’s it.

public protocol CodingKey : CustomDebugStringConvertible, CustomStringConvertible, Sendable {
    var stringValue: String { get }
    init?(stringValue: String)
    var intValue: Int? { get }
    init?(intValue: Int)
}

The Littlest Type

Sometimes there’s code so commonplace that we forget how strange it actually is. I mean, Swift is a strongly typed language right? Types types types! We say what things are, and the compiler enforces it for us. But then you see some piece of code like this:

func addOne(_ x: Int) -> Int {
    fatalError("Haha! No Int for you!")
}

That’s legal Swift. I don’t think many would find that surprising. Of course that’s legal, right? But it is surprising. addOne claims to be a function that accepts an Int and returns an Int. It does accept an Int, but…it doesn’t return an Int.

“Don’t be silly, Rob. It crashes. It can’t return an Int if it crashes.”

Well, yeah. But it promised to return an Int. It didn’t promise to “return an Int or crash,” did it? The whole point of strong types was that the compiler would enforce our promises, and the compiler doesn’t bat an eye at this code.

“The compiler can’t possibly know everything that might crash.”

I’m not ready to concede that, but even so, the compiler clearly can see that this function doesn’t return an Int. There’s no return intValue anywhere. This should prick our ears a bit. Something is strange here. Is it just compiler magic, or is there something deeper?

Hint: There’s something deeper.

Protocols v: At Your Request

So, back to our APIClient. When last I left off, I had the following client code:

final class APIClient {
    static let shared = APIClient()
    let baseURL = URL(string: "https://www.example.com")!
    let transport: Transport

    init(transport: Transport = URLSession.shared) { self.transport = transport }

    // Fetch any Fetchable type given an ID, and return it asynchronously
    func fetch<Model: Fetchable>(_ id: Model.ID,
                                 completion: @escaping (Result<Model, Error>) -> Void)
    {
        // Construct the URLRequest
        let url = baseURL
            .appendingPathComponent(Model.apiBase)
            .appendingPathComponent("\(id)")
        let urlRequest = URLRequest(url: url)

        // Send it to the transport
        transport.send(request: urlRequest) { data in
            let result = Result { try JSONDecoder().decode(Model.self, from: data.get()) }
            completion(result)
        }
    }
}

This fetch method is great for getting a model by ID, but I have other things I want to do. For example, I’d like to periodically POST to /keepalive and return if there’s an error. That’s really similar, but kind of different.

Protocols III: Existential Spelling

This was supposed to be a quick sidebar, but it turned into a full-length article, so I’m calling it part 3. The original part 3, continuing the network stack, is mostly done, but I wanted to explain this weird word “existentials” first, and it turned out longer than I’d expected. Blame Joe Groff; he’s written too much interesting stuff lately and I want to talk about it.

If you’re interested in the future of generics in Swift, Joe Groff has a must-read post called Improving the UI of generics. (You should also read the linked Generics Manifesto for background.) In it, he touches on a common confusion in Swift. If you don’t understand what he’s talking about here, don’t worry. Explaining this paragraph is the point of this article.

We gave existential types an extremely lightweight spelling, just the bare protocol name, partially following the example of other languages like Java and C# where interfaces also serve as value-abstracted types, and partially out of a hope that they would “just work” the way people expect; if you want a type that can hold any type conforming to a protocol, just use the protocol as a type, and you don’t have to know what “existential” means or anything like that. In practice, for a number of reasons, this hasn’t worked out as smoothly as we had originally hoped. Although the syntax strongly suggests that the protocol as a constraint and the protocol as a type are one thing, in practice, they’re related but different things, and this manifests most confusingly in the “Protocol (the type) does not to conform to Protocol (the constraint)” error.

Protocols II: A Mockery of Protocols

In the last section, I ended my little network stack at this point:

// Something that can be fetched from the API
protocol Fetchable: Decodable {
    static var apiBase: String { get }  // The part of the URL for this type
}

// A client that fetches things from the API
final class APIClient {
    let baseURL = URL(string: "https://www.example.com")!
    let session: URLSession = URLSession.shared

    // Fetch any Fetchable type given an ID, and return it asynchronously
    func fetch<Model: Fetchable>(_ model: Model.Type, id: Int,
                                 completion: @escaping (Result<Model, Error>) -> Void)
    {
        // Construct the URLRequest
        let url = baseURL
            .appendingPathComponent(Model.apiBase)
            .appendingPathComponent("\(id)")
        let urlRequest = URLRequest(url: url)

        // Send it to URLSession
        let task = session.dataTask(with: urlRequest) { (data, _, error) in
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                let result = Result { try JSONDecoder().decode(Model.self, from: data) } 
                completion(result)
            }
        }
        task.resume()
    }
}

This can decode any Fetchable model from an API endpoint that has a URL something like https://<base>/<model>/<id>. That’s pretty good, but we can do a lot better. A natural first question is “how do I test it?” It relies explicitly on URLSession, which is very hard to test against. A natural approach would be to create a protocol to mock URLSession.

I hope by the time you’re done with this series, hearing “create a protocol to mock” makes you flinch just a little.

Protocols Sidebar I: Protocols Are Nonconformists

Last time, I mentioned something in passing:

I need a new protocol.

protocol Fetchable: Decodable {
    static var apiBase: String { get }
}

I need a protocol that requires that the type be Decodable, and also requires that it provide this extra string, apiBase.

Read that again. It requires that the type be Decodable and also requires other things. I didn’t say that Fetchable is Decodable. It isn’t.

Protocols I: "Start With a Protocol," He Said

In the beginning, Crusty

In 2015, at WWDC, Dave Abrahams gave what I believe is still the greatest Swift talk ever given, and certainly the most influential. ”Protocol-Oriented Programming in Swift,” or as it is more affectionately known, “The Crusty Talk.”

This is the talk that introduced the phrase “protocol oriented programming.” The first time I watched it, I took away just one key phrase:

Start with a protocol.

And so, dutifully, I started with a protocol. I made a UserProtocol and a DocumentProtocol and a ShapeProtocol and on and on, and then started implementing all those protocols with generic subclasses and eventually I found myself in a corner.

Protocol 'P' can only be used as a generic constraint because it has Self or associated type requirements

And then I started throwing things.