Using Tagged to write safer Swift code

A guy hidden in a repository costume

I recently started a tiny side project that implements the Beanstalk protocol. If you’ve never heard of beanstalkd, it’s a work queue that’s really convenient for simple projects: you post jobs to queues and those jobs are picked up and handled by workers. Each job that you post to a queue has a priority that goes from 0 (urgent) to 4,294,967,295 (not so much). In Swift, this can be represented with a UInt32, but let’s see how we can use Tagged to make it safer and more expressive, starting with an example from a real-world codebase.

Tag, you’re it

Tentacle is a Swift library you can use to talk to the GitHub API, built on top of ReactiveSwift. This library provides a lot of models like User or Repository. All of these models have a numeric Identifier:

struct User {
    let id: Int
}

struct Repository {
    let id: Int
}

While a User’s id or a Repository’s id both use Int as the underlying type, they are both fundamentally different concepts: you can’t fetch a repository’s details using a user’s id, and vice-versa. Well, you can, but in the best case scenario the repository won’t exist, in the worst case scenario, you’ll get a result that doesn’t make sense.

Fortunately, Swift’s type system allows you to prevent this particular issue. A little while ago, when we moved to the new Decodable APIs (one of my most interesting contribution!) that Swift 4 provided, we also added a new type called Identifiable:

struct ID<Of: Identifiable> {
    let rawValue: Int
}

That way, instead of relying on Int, we’re relying on an ID type:

struct User {
    let id: ID<User>
}

struct Repository {
    let id: ID<Repository>
}

Which in turns, allows us to write safer, more expressive APIs:

// Synchronous and using a simplified return type
func fetchRepository(id: ID<Repository>) -> Repository? {}

This approach is only for identifiers but there are actually a lot of types that are far too general for this domain. The concept behind Tagged is the same approach extended to more than just identifiers.

struct Tagged<Tag, Value> {
  let rawValue: Value
}

In our case, we could replace our ID type with a Tagged<Tag, Value> where Tag is a User and our underlying value is Int, like so:

struct User {
    let id: Tagged<User, Int>
}

We could also express an email address or a session duration (in seconds) using the same approach:

enum EmailTag {}
typealias Email = Tagged<EmailTag, String>

enum SecondsTag {}
typealias Seconds = Tagged<SecondsTag, Double>

struct User {
    let email: Email
    let duration: Seconds
}

If you’re looking for more example of this concept, I strongly recommend the following posts and videos:

In fact, I would even recommend using pointfree’s own implementation of Tagged. While, as we showed earlier, the Tagged type itself is simple to describe, their implementation offers additional conformance to Equatable, Hashable, Comparable, Codable and more…

Priority

Switching back to our UInt32 priority from the beginning of this post, we could have simply relied on UInt32 to express our command’s priority, but Tagged makes for a safer API, without impacting the readability of the code that uses it:

enum PriorityTag {}
typealias Priority = Tagged<PriorityTag, UInt32>

func send(message: Message, priority: Priority) {
    // TODO
}

send(message: msg, priority: 2000)

Finally, in the Beanstalk protocol, a message with a priority of 0 is an urgent message. While we could use the raw value of 0, we can also extend Taggedto make for an even nicer API:

extension Tagged where Tag == ProducerCommand.PriorityTag, RawValue == UInt32 {
    static let urgent = Tagged(rawValue: 0)
}

That way, the following API:

send(message: msg, priority: 0) // This is an urgent message

…becomes:

send(message: msg, priority: .urgent) // This is an urgent message

As a bonus, whenever a developer wants to rely on this type, the Tagged part is hidden. They can define a list of priorities to use, like so:

struct MyProject {
    static let imageResizeJob: Priority = 10
    // or
    static let userExportJob = Priority(rawValue: 20)
    // or
    static let superImportantTask = Priority.urgent
}

Wrapping up

I’m at the beginning of my implementation of the beanstalk protocol, but I do think that relying on string types allows us to expose a nice API.