Snippets

Alex Lynch Create NSPredicate from KeyPath

Created by Alex Lynch last modified

Create NSPredicate from KeyPath

  • Proposal: SE-00XX
  • Authors: Alex Lynch,
  • Review Manager: TBD
  • Status: Awaiting implementation

Introduction

This proposal aims to improve the type safety and integrity of NSPredicates by founding them in KeyPaths instead of Strings.

Swift-evolution thread: [Pitch] KeyPath -> NSPredicate, securely

Motivation

NSPredicate string literals are a notorious source of faulty refactors. Consider the following model:

class Person {
    var name: String
    var age: Int
    var friends: Set<Person>
}

Common usage of NSPredicate might look like this:

let findByName = NSPredicate(format: "name == %@", argumentArray: ["John Smith"])

However if the model were later refactored for greater clarity such that name becomes fullName, then the predicate will fail at runtime.

Other examples of potential runtime-only errors include type mismatches and simple typos:

let findByAge = NSPredicate(format: "age == %@", argumentArray: ["John Smith"]) // type mismatch
let findByFriends = NSPredicate(format: "frnds < 2", argumentArray: nil) // property name misspelled

Proposed solution

This proposal suggests enhancing NSPredicate to accept swift's KeyPath type in its %K format. NSPredicate currently accepts only strings. The following is currently possible:

let findByName = NSPredicate(format: "%K == %@", argumentArray: [#keyPath(Person.name), "John Smith"]) // works

The above example is safe against typos and refactors of property names but is not resilient against type mismatch errors.

The following is the proposed enhancement:

let findByName = NSPredicate(format: "%K == %@", argumentArray: [\Person.name, "John Smith"]) // currently does not work

The difference between these two forms is subtle but important. In the second, proposed form the key path is supplied as a KeyPath value not as a #keyPath literal. This difference allows for the creation of an entirely new syntax for predicate description:

let findByName: NSPredicate = \Person.name == "John Smith"
let findByAge: NSPredicate = \Person.age >= 30
let findByFriends: NSPredicate = \Person.friends < 2

The new syntax can be combined with the usual logical operators:

let findPotentialDinnerGuests: NSPredicate = \Person.age => 25 && \.friends < 2 && \.name != "John Smith" 
// don't invite John to the party

And of course the syntax could be very happily married to CoreData:

let peopleToInvite = Person.find(in: myMOC, where: \.age => 25 && \.friends < 2 && \.name != "John Smith" )
// This search is resilant against name refactors, type mismatches and typos.

An example of such this kind of KeyPath manipulation can be found here. Credit Kishikawa Katsumi.

(N.B. This proposal does not include the design of such a keypath library, only the necessary extension to NSPredicate's %K format.)

Detailed design

See section "Alternatives considered" for a discussion comparing NSPredicate's %K format to NSPredicate(block:) constructor.

Source compatibility

The proposed change has no effect on source compatibility.

Effect on ABI stability

The proposed change should have no effect on ABI stability.

Effect on API resilience

The proposed feature has no effect on public API.

Alternatives considered

Two alternative designs were considered.

Alternative 1: KeyPath._kvcKeyPathString

KeyPath privately declares _kvcKeyPathString, which ostensibly is the exact value needed to pass to a %K format. Publicizing this value (with a sensible name) was considered as an alternative, however this approach might have security/stability implications and was therefor rejected. Passing a KeyPath directly to NSPredicate keeps the internals of the matter private.

Alternative 2: NSPredicate(block:)

NSPredicate provides a constructor that uses a supplied block to evaluate the subject. This capability is semantically sufficient to achieve the kind of typesafe KeyPath-to-NSPredicate library that is described in the "Proposed solution" section. However the block constructor for NSPredicate is not supported by CoreData because it would force the reification of every candidate object in the store, which is untenably slow. The proposed design allows for CoreData to perform its usual conversion of NSPredicates to SQL queries.

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.