Predicates in Swift
Discover page available: GenericsEven though modern apps tend to rely heavily on some form of server component in order to load their data and perform various kinds of work, it’s also incredibly common for an app to have to deal with significant amounts of locally stored data as well.
Not only do we have to come up with efficient and secure ways to store such data, we also have to design APIs for accessing it — and to ensure that those are flexible enough to let us keep iterating on new features and capabilities in a smooth and productive manner.
This week, let’s take a look at how we could leverage the power of predicates to achieve a great degree of flexibility when it comes to working with local data, and how Swift enables us to model predicates in a way that’s both highly expressive and very powerful.
Filtering a collection of models
As an example, let’s say that we’re building a classic todo app that lets our users organize their tasks and todo items using multiple lists — each of which is represented by a TodoList
instance.
Since we want to enable our users to easily filter their tasks in various ways — we’ve created several APIs that let us query a list’s items based on a number of different conditions, like this:
extension Date {
static var now: Date { Date() }
}
struct TodoList {
var name: String
private var items = [TodoItem]()
func futureItems(basedOnDate date: Date = .now) -> [TodoItem] {
items.filter {
!$0.isCompleted && $0.dueDate > date
}
}
func overdueItems(basedOnDate date: Date = .now) -> [TodoItem] {
items.filter {
!$0.isCompleted && $0.dueDate < date
}
}
func itemsTaggedWith(_ tag: Tag) -> [TodoItem] {
items.filter { $0.tags.contains(tag) }
}
}
Note that simply calling filter
on an array is often not the most efficient way to search through a (potentially large) collection of values. However, we’ll stick with filter
in this article for simplicity. For more advanced ways of structuring data, check out “Picking the right data structure in Swift”.
While there’s nothing wrong with the above APIs, it would be nice to have a slightly greater degree of flexibility as we keep iterating on our app and as more filtering features are added. As our TodoList
type is currently designed, every time we want to add a new way of filtering our data, we have to go back and add a brand new API.
Let’s see if we instead can find a way to let the consumers of our data decide exactly how they’d like to filter it. That way we would only have to build and maintain a single API, while also enabling new features to be built without any modifications to our underlying datastore.
A case for predicates
One way that we could achieve such a level of flexibility is by using Foundation’s NSPredicate
class, which leverages the dynamic nature of Objective-C in order to let us filter collections of data using string-based queries. For example, here’s how we could use an NSPredicate
to retrieve all overdue items from an array of TodoItem
values:
// Before we can apply a predicate to our array, we must
// first convert it into an Objective-C-based NSArray:
let array = NSArray(array: items)
let overdueItems = array.filtered(using: NSPredicate(
format: "isCompleted == false && dueDate < %@",
NSDate()
))
While NSPredicate
is incredibly powerful, it does come with a fair amount of downsides when used in Swift. First of all, since queries are written as strings, they don’t come with any sort of compile-time safety — as there is no way for the compiler to validate our property names, or even verify that our queries are syntactically correct.
Using Objective-C based APIs, like NSPredicate
and NSArray
, also requires us to turn our data models into classes that inherit from NSObject
— which would prevents us from using value semantics, and require us to conform to Objective-C conventions, like enabling dynamic string-based access to our properties.
So while the idea and concept of predicates is incredibly appealing, let’s see if we can implement them in a more “Swifty” way. After all, implementation details aside, a predicate is just a function that returns a Bool
for a given value — and could, in the very simplest of forms, be modeled like this:
typealias Predicate<T> = (T) -> Bool
While we could definitely define our predicates as free-form functions and closures, let’s instead create a simple wrapping struct that takes a closure and stores it as its matcher:
struct Predicate<Target> {
var matches: (Target) -> Bool
init(matcher: @escaping (Target) -> Bool) {
matches = matcher
}
}
If the above design looks similar, it might be because we used the exact same setup to implement plugins in last week’s article, “Making Swift code extensible through plugins”.
With the above in place, we can now go back to our TodoList
type and replace all of our previous filtering APIs with just a single one — that takes a Predicate
and passes its matching closure to items.filter
in order to return the items that we’re looking for:
extension TodoList {
func items(matching predicate: Predicate<TodoItem>) -> [TodoItem] {
items.filter(predicate.matches)
}
}
Now, whenever we want to filter a TodoList
based on any sort of query, we can now easily do so by calling the above method:
let list: TodoList = ...
let overdueItems = list.items(matching: Predicate {
!$0.isCompleted && $0.dueDate < .now
})
However, while we’ve now made our filtering API a lot more flexible, we’ve also lost some of the consistency that our previous approach gave us. If we would need to re-type the same predicate code over and over again, chances are high that we’ll end up with inconsistencies, bugs and mistakes.
Thankfully, there’s a way to both achieve a decent level of consistency, while still maintaining the power and flexibility of free-form predicates. By combining static properties and factory methods with generic type constraints, we can construct our most commonly used predicates in one central place — like this:
extension Predicate where Target == TodoItem {
static var isOverdue: Self {
Predicate {
!$0.isCompleted && $0.dueDate < .now
}
}
}
Not only does the above approach give us a much greater degree of consistency, it also makes our call sites look really elegant, since we can reference the above kind of static properties using dot-syntax:
let overdueItems = list.items(matching: .isOverdue)
Now that we’ve found a way to make our new predicate system both consistent and flexible — let’s see if we can keep iterating on it to make it even easier to define and compose various predicates.
Expressive operators
One of the perhaps most divisive features that Swift offers is the ability to define and overload operators. While there’s definitely an argument to be made that relying too heavily on (especially custom) operators can make our code much more cryptic and harder to understand — they could also have the complete opposite effect when used within the right contexts.
For example, let’s see what would happen if we would overload the ==
operator to enable a predicate to be defined using a key path and a value to match against:
func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate { $0[keyPath: lhs] == rhs }
}
In Swift, operator implementations are just normal functions that take their operands as arguments (in this case a left-hand side one, and a right-hand side one).
Using the above, we can now create matching predicates like this:
let uncompletedItems = list.items(matching: \.isCompleted == false)
That’s pretty cool! If we wanted to, we could also overload the !
operator as well — which would enable us to encapsulate the above == false
check within the resulting predicate:
prefix func !<T>(rhs: KeyPath<T, Bool>) -> Predicate<T> {
rhs == false
}
With the above in place, our call site syntax now becomes really lightweight:
let uncompletedItems = list.items(matching: !\.isCompleted)
Along the same lines, we could also overload the >
and <
operators for values that conform to Swift’s Comparable
protocol — enabling us to define comparing predicates using the same lightweight syntax:
func ><T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate { $0[keyPath: lhs] > rhs }
}
func <<T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> Predicate<T> {
Predicate { $0[keyPath: lhs] < rhs }
}
let highPriorityItems = list.items(matching: \.priority > 5)
The fact that operators allow us to express completely custom types purely using language syntax is really fascinating, and gives us an incredible amount of power when it comes to how we design our APIs. We just have to make sure to use that power responsibly.
Composition built-in
The fact that we chose to base our Predicate
type on closures also opens up a lot of opportunities for composition — since functions and closures can easily be combined in order to form new functionality.
For example, here’s how we could define an additional pair of operators to enable our predicates to be composed into double-matching, or either-matching combinations:
func &&<T>(lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
Predicate { lhs.matches($0) && rhs.matches($0) }
}
func ||<T>(lhs: Predicate<T>, rhs: Predicate<T>) -> Predicate<T> {
Predicate { lhs.matches($0) || rhs.matches($0) }
}
Using the above, along with the other operator overloads that we’ve defined so far, we can now create much more complex predicates that can have any number of conditions — all using existing operators that we’re already familiar with:
let futureItems = list.items(
matching: !\.isCompleted && \.dueDate > .now
)
let overdueItems = list.items(
matching: !\.isCompleted && \.dueDate < .now
)
let myTasks = list.items(
matching: \.creator == .currentUser || \.assignedTo == .currentUser
)
However, going back to the point of consistency from before, when it comes to complex predicates that are intended to be reused throughout our code base, it’s arguably better to still use a static factory method — as doing so lets us neatly encapsulate all of the associated logic in a single place:
extension Predicate where Target == TodoItem {
static func isOverdue(
comparedTo date: Date = .now,
inlcudingCompleted includeCompleted: Bool = false
) -> Self {
Predicate {
if !includeCompleted {
guard !$0.isCompleted else {
return false
}
}
return $0.dueDate < date
}
}
}
let overdueItems = list.items(matching: .isOverdue())
By using a method, rather than a computed property, we can also inject customization options and data that can be stubbed when writing tests.
One of the major benefits of using a wrapping type, rather than referencing functions and closures directly, is that doing so enables us to decide which kind of syntax that’s the most appropriate within each situation.
An expandable pattern
The beauty of the predicate pattern that we’ve now established is that it enables us to keep expanding our implementation to suit our evolving needs. For example, if we found ourselves needing to check whether a given nested collection contains an element, we could add yet another operator overload for that:
func ~=<T, V: Collection>(
lhs: KeyPath<T, V>, rhs: V.Element
) -> Predicate<T> where V.Element: Equatable {
Predicate { $0[keyPath: lhs].contains(rhs) }
}
let importantItems = list.items(matching: \.tags ~= "important")
Finally, since we built our Predicate
type as a simple closure-based wrapper, it’s automatically compatible with many of the standard library’s various collection APIs — as we can pass a predicate’s matches
closure as a first class function:
let strings: [String] = ...
let predicate: Predicate<String> = \.count > 3
strings.filter(predicate.matches)
strings.drop(while: predicate.matches)
strings.prefix(while: predicate.matches)
strings.contains(where: predicate.matches)
The above call sites also read really nicely, almost like normal English sentences, thanks to our use of a wrapping type and the way its matches
property is named.
Conclusion
Predicates can enable us to filter through in-memory data in ways that are both flexible and incredibly powerful, without requiring us to maintain a growing set of highly specific APIs.
By modeling our predicates in a way that takes advantage of some of Swift’s most powerful features — like generics, operators, and first class functions — we can also create predicates that are not only flexible, but completely type-safe as well.
I’m also really looking forward to the implementation of Swift Evolution proposal SE-0249, which will enable key paths to be directly converted into functions — enabling even more kinds of predicates to be easily constructed.
However, the kind of predicates that we explored in this article are really only suitable for querying in-memory data, as it’d be very hard to encode free-form closures into something that could be passed to a server, or to an on-disk database.
What do you think? Have you used predicates in this sort of way before, or is it something that’d be suitable for your code base? Let me know — along with your questions, comments or feedback — either via Twitter or email.
Thanks for reading! 🚀