Constant Keys (Sometimes It's The Little Things)
December 09, 2019

Bookish Development Diary, episode 3.

In which our intrepid developer disappears down a deep rabbit hole, in search of the cleanest and most idiomatic way to express string constants in Swift1. Because, you know, stuff…

Sometimes it’s the little things that make me happiest.

In an open-ended system like Datastore, where any property can be assigned to any entity, I needed to be able to specify a property using some sort of key.

It turns out that there are a few ways of doing this…

A key for a Datastore property could theoretically be anything hashable. As is often the case with the standard Dictionary type though, the most natural fit for these keys is simply String.

Strings are easy to express (and read) in code, and map well to textual representations such as XML or JSON (which is important for Datastore’s interchange format).

You can just type these in your code where you need them.

store.add(properties: [person: ["name": "Fred"]]) { results in
    // some time later, use it...
    store.get(properties: ["name"], of: [person]) { properties in
        // do something with the properties
        if let name = properties.first?["name"] as? String {
            print(name)
        }
    }
}

Anyone with more than a passing exposure to good coding practice might be getting twitchy at the prospect of repeating those string literals. It violates the DRY principle, and opens us up to nasty like bugs like this…

store.add(properties: [record: ["complicatedKey": someValue]]) { results in
    // ...
    store.get(properties: ["complciatedKey"], of: [record]) { properties in
        // ...
        }
    }
}

Better to use a constant. In a local scope, you might just do this sort of thing

let nameKey = "name"

// make a record
store.add(properties: [person: [nameKey: "Fred"]]) { results in
    // some time later, use it...
    store.get(properties: [nameKey], of: [person]) { properties in
        // do something with the properties
        if let name = properties.first?[nameKey] as? String {
            print(name)
        }
    }
}

However, although Datastore is generic, your use case is specific. You will probably have a set of commonly used keys. So typically the solution is to define a load of string constants at a higher scope. You might do something like this:

struct Model {
    struct Keys {
        static let name = "name"
        static let address = "address"
        // ...etc
    }
}

then use it like this:

// make a record
store.add(properties: [person: [Model.Keys.name: "Fred"]]) { results in
    // some time later, use it...
    store.get(properties: [Model.Keys.name], of: [person]) { properties in
        // do something with the properties
        if let name = properties.first?[Model.Keys.name] as? String {
            print(name)
        }
    }
}

This is better, but there are some niggles. It feels a little bit repetitious having to say Model.Key every time.

What’s worse, the Swift compiler has no clue that Model.Key has anything to do with your API, so Xcode can’t auto-suggest it, nor can it auto-suggest the constants themselves until you’ve typed the Model.Key bit.

You could just do let name = "name" at the global scope, but then you’re polluting the global namespace which has its own set of issues (such as Xcode suggesting name for any API that takes a string, anywhere).

What would be nice is to be able to do this:

// make a record
store.add(properties: [person: [.name: "Fred"]]) { results in
    // some time later, use it...
    store.get(properties: [.name], of: [person]) { properties in
        // do something with the properties
        if let name = properties.first?[.name] as? String {
            print(name)
        }
    }

and be able to type ‘.’ and have Xcode suggest suitable constants, but also be able to just type a string literal instead.

So how to achieve that?

For Xcode to suggest one of our key constants, it needs to know that we are typing a key. For that to happen, the key needs to be a custom type, and the API has to be written to use that type instead of a string.

Those .name entries in the code sample above look awfully like cases in an enum. Maybe if we made the key a string-backed enum:

enum Key: String {
    case name = "name"
    case address = "address"
        // ... other keys...
}

That sounds like what we want, right? We can define the keys we know about. They are still actually strings, so converting to/from interchange should work ok.

Except that we’ve forgotten something. One of our requirements is that Datastore supports any key, not just the ones we happen to need for a particular scenario.

We can turn raw strings into our enum type with Key(rawValue:) initialiser, but it returns an optional, and that optional will be nil if we try to give it a key that we didn’t make a case for. Yet clearly we can’t make an infinite number of cases.

Back to square one.

So if we can’t use an enum, maybe we just need a custom type, which contains the string?

struct Key: Hashable {
    let value: String
}

This is a custom type, so the compiler can make sensible suggestions when it is expecting one.

It isn’t an enum with a finite set of cases, so we can define them as constants, but also make them from dynamic strings when we need to.

But what about that .name syntax? How do we get the compiler to support that, without it being an enum.

It turns out that what the compiler is doing in those enum cases isn’t actually because they are enums. It is an example of a feature of Swift called Implicit Member Expressions.

What this boils down to is that when the swift compiler knows that the type of the value it is expecting is Foo, and you supply it with .bar, it can infer that what you actually meant was Foo.bar.

In the case of our enum example above, this means that .name is treated as Key.name, which is the name case of the Key enum, and so works.

So for our Key struct, we just need to make sure that Key.name resolves to a Key where the value property is “name”.

Which is easily achieved by defining a static on Key. What’s really nice is that we can do this in an extension:

extension Key {
    static let name = Key(name: "name")
}

This is pretty close to where we wanted to be.

The .name constant now works wherever the compiler is expecting one of our keys.

Having the definition in an extension feels right too - it can live in a logical place which collects together all the keys for our use case.

There’s just one other thing that would be nice.

Sometimes in our code we will have places where we only need to use a key once. Maybe in a test, or maybe in a place where the value is set externally, and all our code ever does is read it.

Having to define a constant, or create a Key(value: "one-shot key") object, feels a little clumsy here. It would be nicer if we could just use a string literal, and have the compiler work it out.

Knowing what we now know about implict member expressions, we might be temnpted to do away with our custom class and go back to just using strings. We can get that .name behaviour that we wanted by defining our static on String itself.

extension String {
    static let name = "name"
}

Now we can use .name for a key, but also just supply a literal.

This works, but what we’ve just done is thrown away useful type information that we were giving the compiler by defining the Key type, and also spammed the String type with a constant that will be suggested anywhere that Xcode expects a String.

So let’s not do that.

What we need instead is to use another Swift feature: Initialisation With Literals. What this gives us is the ability to have the compiler automatically create a custom type from a string literal.

All our custom type needs to do is to adopt the ExpressibleByStringLiteral protocol, and implement init(stringLiteral: String).

Now, when we know that a key is going to be repeated, we can define a constant like name, and supply it to the API as .name, but when we have a value we’re only using once, or when we’re writing test code or just trying an experiment, we can just supply the string "one-shot key" instead, and everything will still work.

One little side-benefit of this is that we can rewrite our constant definitions to be a little less wordy:

extension Key {
    static let name: Key = "name"
    static let address: Key = "address"
}

So there we are. The route was perhaps a little circuitous, but what we’ve ended up with feels nice, and clean, and hopefully reads like good idiomatic Swift.


  1. If you’re a sophisticated user of Swift, none of what I’m about to say will probably come as a surprise. You may be bored or underwhelmed by this tale. Sorry! In fact, maybe you know a better solution? If so, please let me know. The solution I describe is was one of those things that I had to puzzle out over time when I started writing Swift - mostly by seeing other code that looked cleaner than mine, and figuring out what the difference was. I’ve been meaning to write it down explicitly for a while. 

« No API Survives Contact With The E̶n̶e̶m̶y̶ Client Further Evolution »
Got a comment on this post? Let me know at @samdeane@mastodon.org.uk.