Alexito's World

A world of coding 💻, by Alejandro Martinez

Better Dictionary literals with Result Builders

Swift is well known for its lean syntax that helps newcomers understand the code relatively quickly. But it's also a language with advanced and powerful features. Sometimes this makes part of the community wonder if it is going too far. In this post, I'm glad to merge these two worlds and use an "advanced" feature of the language to improve the base syntax. Of course I'm talking about Result Builders and a DSL for Dictionary literals.

Dictionary literals

One place where we've grown accustomed to a nice and simple syntax is literals. You can create a dictionary literal with keys and values easily. If you don't have experience with other languages, you may not realise how nice it is.

[
    "key": value,
    "hello": "world"
]

But often you may need to only add keys to the dictionary based on some condition, like if the value is not nil. And when that happens, the literals don't make the cut anymore.

You need to start using the Dictionary API to add values after its original creation. But that means that you need to have a variable to mutate. And not only that, but now you need to put the constant values on the literal init and anything that requires logic after that. This is quite annoying when you want to define the dictionary with a specific order to make it easier to read.

var dictionary = [
    "key": value
]
if let welcome = optionalMessage {
    dictionary["hello"] = welcome
}
...

Of course, this is not a big deal with such a simple example. But in reality, the function where this code lives is probably more complex and having to keep a variable just for this adds extra complexity. What if we could just make the literal initialisation support conditions?

Result builders

Result builders is a Swift feature introduced by SwiftUI and officially added to the language on Swift 5.4. I talked about it in the context of the SwiftUI DSL but also as a deep dive on result builders feature on itself. Because this feature is much more than just an implementation detail of SwiftUI. It allows us to make very nice DSLs.

People in our community always have a tendency to look bad at these more advanced, Swift features. This one specifically is always countered with the question of what utility does it have outside SwiftUI. And it's true that the typical examples are all about a very similar use case, like building HTML. But there are other interesting applications of it and the one I'm proposing here is not even to improve a separate framework, but the language itself. You can't get more useful than that!

DictionaryBuilder

Let's build a result builder that helps us make dictionaries. And the nice thing is that is pretty easy.

I won't go into much detail of how Result Builders work. I recommend you to watch my deep dive video to get a better understanding.

@resultBuilder
struct DictionaryBuilder<Key: Hashable, Value> {
  ...
}

To start, we just define a type and annotate it with the special attribute @resultBuilder. This tells the compiler that this type can be used as an annotation of its own to enable the special DSL syntax in the body of a function.

Then we need to make the builder do something useful. We do this by adding special functions.

static func buildBlock(_ dictionaries: Dictionary<Key, Value>...) -> Dictionary<Key, Value> {
    dictionaries.reduce(into: [:]) {
        $0.merge($1) { _, new in new }
    }
}

This will make the builder accept dictionaries. But with this we gain very little. We could merge multiple dictionaries easily, but that's not our goal. To support conditionals, we need to add another special function.

static func buildOptional(_ dictionary: Dictionary<Key, Value>?) -> Dictionary<Key, Value> {
    dictionary ?? [:]
}

With these two simple functions, we have unlocked a huge potential of this feature. There are way more things a function builder can do but for now lets stick to the scenario that we want to solve.

Using the new builder

Now we have a new annotation available: @DictionaryBuilder<Key, Value>. To use it one approach is to make a new Dictionary initialiser that let's use use this new DSL.

extension Dictionary {
  init(@DictionaryBuilder<Key, Value> build: () -> Dictionary) {
      self = build()
  }
}

This is quite simple. The only thing we're doing is to mark the closure parameter as a builder. This indicates the compiler that the body of that closure will have super powers.

let dictionary = Dictionary<String, String> {
    [
        "id": "1",
        "name": "alex",
    ]
    if let welcome = optionalMessage {
        ["hello": welcome]
    }
    [
        "more": "values"
    ]
}
...

This allows us to use a let, which simplifies our code. It also lets us mix literal dictionaries with keys that require some logic, which lets us keep the order of the keys like we want it. This may seem like a trivial thing, but in some occasions it helps tremendously to improve the readability of the dictionary construction.

But there is another way of using the builder. If you have watch my AnyView vs. @ViewBuilder video you probably know what I'm talking about. The dictionary initialiser is very useful if you are in the middle of a bigger function, but if your function is just there to return the resulting dictionary you can't skip the initialiser altogether and use the DSL directly!

@DictionaryBuilder<String, String>
func makeMeADictionary() -> Dictionary<String, String> {
    [
        "id": "1",
        "name": "alex",
    ]
    if let welcome = optionalMessage {
        ["hello": welcome]
    }
    [
        "more": "values"
    ]
}

Conclusion

We have seen how this Swift feature is not only there to make SwiftUI nice to use, but it can unlock a huge potential to improve our code. DictionaryBuilder is not just making the syntax nicer, but is helping to make our code easier to understand thanks to avoiding variables and letting us define the keys in the order we want.

And this is just the tip of the iceberg. Function builders are even more powerful than this, and I'm sure we will find more incredible applications. I think we need to stop complaining about the complexity of new features as long as the learning curve is not impacted. Some of this features are crucial to make Swift a better language and even to simplify the barrier of entry. Keep your mind open folks ;)

If you liked this article please consider supporting me