[Draft] Create NSPredicate from KeyPath

(Alex Lynch) #1

Hello All,

Thanks for the initial feedback on creating compiler-checked NSPredicates from KeyPaths.

Here is the draft proposal: https://bitbucket.org/snippets/lynchrb/oe8qzk


Create NSPredicate from KeyPath

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


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


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.

Where Do Proposals For Improvements to Apple Swift API wrappers go?
(Alexey Kravchenko) #2

+1, not all in this problem is clear yet, but good start in any case.

(Alexey Kravchenko) #3

Well, I think we have two independent parts in this proposal.
First is to make KeyPath and %K compatible. I’d prefer implicity solution (without string getter for KeyPath) but this could led to Core Data proprietary code changes and we can not influence on it (only via Apple bugtracker).
Second part is to improve KeyPath special syntax to be compatible with predicates construction. But we can go even further and implement this also for Swift Collections. Such solution will open interesting perspectives in future.

(Alex Lynch) #4

While both of those things are interesting to talk about, I disagree that they are both in the proposal. The proposal is clear that the desired change is only about KeyPath and %K. The use of special KeyPath syntax is presented only as motivation. I feel that the open source community that surrounds the Swift language proper is the right place to work out the library which implements this syntax.
If you feel that this separation of concerns is not clear in the proposal, then could you suggest how to make it clear?

(Alexey Kravchenko) #5

Yes, you are right. Maybe I was confused by the proposal name at first. But now I see you motivation. All is well thank you.

(Rod Brown) #6

While I think this could really use some work within Foundation, I believe changes to Foundation itself (where NSPredicate resides) are out of scope for Swift Evolution. Updating Foundation to handle key paths like this would, I suspect, be the responsibility of Apple as they own Foundation and it is not open source.

(Alexey Kravchenko) #7

We have swift-corelibs-foundation but in https://github.com/apple/swift-corelibs-foundation/blob/master/Docs/Status.md we see that predicates unimplemented or incomplete now. Also “On macOS, iOS, and other Apple platforms, apps should use the Foundation that comes with the operating system.”.
But at minimum we can discuss how correctly transfer KeyPath to string and make it available for every day Core Data code. In any case that would be useful.

(Alex Lynch) #8

I’m sure the separation between Swift and Foundation is lost on no one here. But I don’t think the appropriate response to that separation is to design around Foundation as though it were not an ally. Surely there is a standard procedure to pass a request from the Swift team to the Foundation team. Yes?

(Xiaodi Wu) #9

I think this is pretty clearly outside the scope of the open source project.

Some changes to Foundation as it relates specifically to Swift’s facilities for interoperability with Foundation have been reviewed here, but you’re proposing a change to an Apple-owned type to enhance Foundation’s facilities for interoperability with Swift, which is entirely controlled by Apple.

(Davide De Franceschi) #10

What if this proposal is interpreted as adding a KeyPath-supported DSL for ease of creating predicates (not NSPredicate), with the bonus point that NSPredicate could hop on this functionality?

(Alexey Kravchenko) #11

Perhaps this would be a good solution for the future. I think that the need for this will appear when Swift strongly go forward to databases area. Now we have IBM solution as some sort of example - https://github.com/IBM-Swift/Swift-Kuery and it is very interesting.

We really could discuss native Swift predicates. But problem with KeyPath and String/%K still stays on.

(Davide De Franceschi) #12

Would making #keyPath working not only with properties references but also with KeyPaths solve that?

(Alexey Kravchenko) #13

Most likely I want to see KeyPath as replacement for #keyPath in predicates. We have #keyPath now and it works very well but KeyPath is more perspective solution because of type checking.

(Alex Lynch) #14

It looks like the most useful thing to do right now is create Radars:
See the end of this Jira discussion: https://bugs.swift.org/browse/SR-5220

(Lazar Otasevic) #15

This is not sufficient.
We need typed-predicate in order not to loose "Root" type information from the KeyPath, and then we can make safe comparison-predicates between them, also typed.