Creating custom query functions with key paths

Since it is a fairly strict statically compiled language, at first glance it may seem that Swift has little to offer in terms of syntax customization, but in reality this is far from the case. Thanks to features such as custom and overloaded operators, key paths, function / result builders and so on, we have many options for customizing Swift syntax for specific use cases.

Of course, it can definitely be argued that any kind of syntax customization should be approached with great care, since non-standard syntax can easily become a source of confusion if we are not careful enough. But in certain situations, this trade-off may well be worth it and may allow us to create something like “micro-DSL“Which can actually help us make our code more understandable, and not vice versa.

Inverted boolean key paths

To address one such case, suppose we are working on an application for managing, filtering and sorting articles that has the following data model (Article):

struct Article {
    var title: String
    var body: String
    var category: Category
    var isRead: Bool
    ...
    
}

Now, suppose a very common task in our codebase is to filter out various collections, each containing instances of the above model. One way to do this is to take advantage of the fact that any key path Swift literal can be automatically converted to function, which allows us to use the following compact syntax when filtering by any boolean property, for example in this case isRead:

let articles: [Article] = ...
let readArticles = articles.filter(.isRead)

This is really cool, however the above syntax can only be used when we want to compare with true – this means that if we wanted to create a similarly filtered array containing all unread article, we would have to use a closure instead (or pass a function):

let unreadArticles = articles.filter { !$0.isRead }

This is certainly not a big problem, but if the operations described above are performed in many different places in our codebase, then we can start asking ourselves: “Wouldn’t it be great if we could also use the same pretty key path syntax for inverted booleans? “

This is where the concept of syntax customization comes in. By implementing the following prefix function, we can actually create a little tweak that will allow us to use the key path regardless of whether we are comparing with true or false:

prefix func !<T>(keyPath: KeyPath<T, Bool>) -> (T) -> Bool {
    return { !$0[keyPath: keyPath] }
}

The above is essentially an overload of the built-in prefix operator !which allows this operator to be applied to any Bool key path to turn it into a function that inverts (or flips) its value, which in turn now allows us to process our array unreadArticles in the following way:

let unreadArticles = articles.filter(!.isRead)

This is pretty cool and doesn’t make our code more confusing, given that we are using the operator ! in a way that matches how it is used by default – to negate a boolean expression.

Comparison based on key paths

We can go even further and also make it possible to use key paths to form filter queries that compare a given property to any kind of value. Equatable… This would be useful if, for example, we wanted to be able to filter an array Equatable for each category (category) articles. The type of this property, Category, is currently defined as an enum, which looks like this:

extension Article {
    enum Category {
        case fullLength
        case quickReads
        case basics
        ...
    }
}

In the same way as we have previously overloaded ! with key path specific, we can do the same with the operator ==, and as before, we will return the returning Bool a closure that can then be directly passed to the API by type filter:

func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] == rhs }
}

With the above in mind, we can now easily filter any collection using a key path based comparison like:

let fullLengthArticles = articles.filter(.category == .fullLength)

Conclusion

Depending on who you ask, the fact that Swift allows us to easily create the above functionality with a couple of light overloads can be either truly awesome or incredibly disturbing. I tend to think that the truth is somewhere in between, and I think it’s really cool that we can make minor domain-specific changes to Swift’s syntax. But at the same time, I think that these changes should always be made in order to make our code easier, not more complex.

Like everything I write about here in Swift by Sundell, I have personally used. I’ve used the above technique on several projects where I had to filter a lot of collections and it worked great, but I wouldn’t use it unless I had a strong need for that functionality.

For a more detailed and more advanced version of the above technique, check out See Predicates in Swift and feel free to send me your questions and comments via Twitter or by e-mail


The translation of the article was prepared in anticipation of the start of the course “IOS Developer. Professional”

  • How popular are iOS developers during the crisis?

  • What are the requirements for jobseekers from employing companies?

  • What questions are asked during the interview, and how to avoid mistakes when answering?

  • What knowledge and skills do you need to stand out from the crowd and secure your career progress?

All these questions, within the framework of a free career webinar, will be answered by our expert – Eksei Panteleev. Eksei will also tell you in detail about the course program and the learning process. Sign up for a webinar

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *