Implementing a Small DSL in Swift

Posted October 28, 2015

I was working on an iOS project earlier today when I ended up running into an annoying bug in my Swift Core Data stack. My seemingly correct NSFetchRequest code was returning nil for all inputs, and I couldn’t figure out why.

After inspecting and thinking through totally different parts of the codebase, I had an ah-hah moment! I went back to my fetch request code and sure enough I saw something resembling the following:

let predicate = NSPredicate(format: "%@ = %@", key, value)

Those acquainted with Core Data will probably notice my error pretty quickly: I accidentally formatted key as a value type! The format string should actually be %K = %@, that way Core Data will know that key is in fact, a substitutable key. Otherwise, it will simply perform string comparison between the two String values.

I lamented that there was no error checking in format strings and quickly moved on. However, I had the idea in the back of my mind that a small, focused, problem-driven language, or Domain Specific Language, could easily be built in Swift to aid in basic validation/cleanliness of NSPredicate construction.

Overview

I figured a limited proof of concept DSL for NSPredicate construction could be made pretty quickly for “fun” 😉 As a baseline, all I wanted to support was:

Referencing keypaths and simple String-convertible constant values

Checking keys against values with comparison operators

Combining expressions with logical operators, such as AND and OR

My goal syntax was something resembling the following:

let predicate = NSPredicate("foo" == true && !("bar" == 4))

Luckily for me, this experiment turned out to be relatively straightforward thanks to many of Swift’s powerful language features. Let’s dive straight into the implementation!

Keys and Values

We’ll start with the most simple object to represent: a keypath. A keypath is simply a string that represents a (potentially nested) property, for example amountDeposited or parent.firstName. To encode this in our DSL, we’ll simply represent a KeyPath as a String:

typealias KeyPath = String

The second most simple object type to represent is a constant value. Again, in our simplified DSL, we’ll only be supporting values that can be directly represented as strings. Thus, we simply want ValueType to be those objects that are CustomStringConvertible:

typealias ValueType = CustomStringConvertible

A more full-featured DSL would not be encoding values into strings, but this representation will suit our small experiment 👍🏼

Operators

Now we can represent the names and values of things we want to query. However, we need a way to relate the two: this is where operators come into play.

We’ll support a small subset of comparison and logical operators for querying data. These can be easily represented by Swift’s ever-helpful RawRepresentable-backed enum type:

Given that we’ll eventually want to convert these Comparison values into a String representation, we’ll adhere to CustomStringConvertible ourselves. Because we’ve already defined a nice string for the rawValue, we’ll simply return that for our implementation.

Tip: We could have easily made calls to rawValue in our code later for String versions of our operators, but I consider implementing CustomStringConvertible a cleaner mechanism. For one, it gives you a free initializer: let str = String(Comparison.EqualTo). But more importantly, it keeps the “storage mechanism” of Comparison abstracted out.

Expressions

The Expression type will represent a value or compound comparison of values that we can convert to an NSPredicate. In our DSL’s simplified model, there are four types of values:

A property

A constant

A negated expression

A compound expression connecting two subexpressions

These concepts can be directly translated into Swift via yet another enum. However, this time we’ll be creating a recursive enumeration:

An Expression of type Negated is simply a subexpression with an additional tag attached saying that it should be negated. Likewise, Binary is composed of two subexpressions related by a Comparison operator. In both of these cases, the subexpressions could be anything from a simple Property to another Binary relation.

Feel free to read up on Algebraic Data Types if you want to learn more about structuring data like this!

At this point, we have enough ground-level types to represent what we want to eventually generate. Next, let’s take a look at how we can build up an NSPredicate.

NSPredicate Creation

Our DSL implementation will create NSPredicates by concatenating String representations of our query together. Keypaths, constants, and operators are already strings (or string convertible) but how do we stringify an Expression?

You might have guessed it: Recursion again!

By defining how to convert the simple cases of Expression to a String (the Property and Constant cases), we can implement the more complicated cases on top. Let’s take a look at the code:

Now that we can fully convert an Expression, our simplified representation of an NSPredicate, to a String, we simply need a clean way to pass it to NSPredicate itself. To do this, we can define a convenience initializer:

Literal protocols will allow us to type a value such as 3 and have it automatically converted to Expression.Constant(3), for example. Likewise, it’d be great if strings were automatically interpreted as keypaths. We support this behavior by adhering to a number of different protocols and defining specialized initializers.

Next, we’ll use (abuse?) some operators for combining expressions. The most obvious way of implementing a simple, readable DSL for predicates is by overloading the relevant comparison and logical operators to work on Expression objects as well.

Implementing the syntactic sugar for this DSL ends up being little more than tying an operator with the matching Expression case.

As a note, it’s always worth thinking whether the above is a good idea, or if you should introduce your own operators instead. The second case should almost always be the preferred solution, but if the semantics of an existing operator fits 100% with your data structures (think a Vector type that could work with +, etc) then it might be okay to overload.

Now that we’ve got some of the philosophy out of the way, let’s give it a spin:

Looks neat, plus you get some minor validation wins done by the compiler. Note, however, it is still entirely possible to construct an invalid NSPredicate with the existing DSL - it’s more demo/thought experiment than production-ready library!

Practically speaking, a similar, type-checked, more fleshed out DSL could perhaps end up being useful for Core Data development, where you are limited to the string parsing predicate API. For other applications, however, block predicates or SequenceType’s filter function will potentially solve the problem better.