[Review] SE-0161: Smart KeyPaths: Better Key-Value Coding for Swift

Example:
struct Foo {
    var firstName: String
    var lastName: String
}

You would need to spell out keypath(Foo, .firstName) here, or decorate the type on keyPaths like so:

let keyPaths : [KeyPath<Foo, String>] = ... or let keyPaths : [PartialKeyPath<Foo>] = ... to use the short hand in your value declaration. The difference between the two is that the PartialKeyPath lets you refer to non-string properties, whereas the other is more strongly typed.

let keyPaths = [keypath(.firstName), keypath(.lastName)]

The same would be true on the inference:

let keyPaths = [keypath(Foo, .firstName), keypath(Foo, .lastName)]

Infers that keyPaths is a [KeyPath<Foo, String>] since everything agrees;

let keyPaths = [keypath(Foo, .firstName), keypath(Foo, .age)] // suppose it returns Int

Would only be able to infer up to PartialKeyPath<Foo>.

···

On Apr 3, 2017, at 1:47 PM, Joshua Alvarado via swift-evolution <swift-evolution@swift.org> wrote:

for key in keyPaths {
    foo[keyPath: key] = "bar"
}

print(foo) // firstName = bar, lastName = bar

You guys aren't thinking big enough.

  // This implementation is closure-based because it's too complex for an email even *with*
  // generics system upgrades. Without type system upgrades, things get *really* complicated,
  // though probably still tractable.
  typealias Predicate<Element> = (Element) -> Bool
  
  func == <Root, Value: Equatable> (lhs: KeyPath<Root, Value>, rhs: Value) -> Predicate<Root> {
    return { $0[keyPath: lhs] == rhs }
  }
  
  func < <Root, Value: Comparable> (lhs: KeyPath<Root, Value>, rhs: Value) -> Predicate<Root> {
    return { $0[keyPath: lhs] < rhs }
  }

  func && <Root>(lhs: Predicate<Root>, rhs: Predicate<Root>) -> Predicate<Root> {
    return { lhs($0) && rhs($0) }
  }

  extension KeyPath where Value: Collection {
    func contains(where predicate: Predicate<Value.Element>) -> Predicate<Value> {
      return { $0.contains(where: predicate) }
    }
  }

That gives you:

  let isPuppyQualifier = keypath(Pet, .type) == .dog && keypath(Pet, .age) < 12
  let familyQualifier = keypath(Family, .pets).contains(where: isPuppyQualifier)
  let familiesWithPuppies = Family.fetch(editingContext, familyQualifier)

Or, in one line with a more sugary syntax:

  let familiesWithPuppies = Family.fetch(editingContext, (.pets).contains(where: .type == .dog && .age < 12))

···

On Apr 1, 2017, at 5:56 PM, Karl Wagner via swift-evolution <swift-evolution@swift.org> wrote:

let isPuppyQualifier = Pet.type.equals(.dog).and(Pet.age.lessThan(12))
let familyQualifier = Family.pets.hasAtLeastOne(satisfying: isPuppyQualifier)
let familiesWithPuppies = Family.fetch(editingContext, familyQualifier)

For those unfamiliar with EOF, the editingContext in the code above is an EOEditingContext which is analogous to NSManagedObjectContext.

Theoretically, you could do something like that with this proposal...

struct UnaryPredicate<Parameter, Result> {
    let evaluate: (Parameter) -> Result
}
struct BinaryPredicate<Left, Right, Result> {
    let evaluate: (Left, Right) -> Result
}

extension KeyPath where Value: Equatable {
    func equals(_ value: Value) -> UnaryPredicate<Root, Bool> {
        return UnaryPredicate { $0[keyPath: self] == value }
    }
    func equals<KP: KeyPath>(_ other: KP) -> BinaryPredicate<Root, KP.Root, Bool> where KP.Value == Value {
        return BinaryPredicate { $0[keyPath: self] == $1[keyPath: other] }
    }
}

let isDog = keypath(Pet, .type).equals(.dog) // UnaryPredicate<Pet, Bool>
if isDog.evaluate(somePet) {
    print(“It’s a dog”)
}

let areSameLength = keypath(Array<Int>, .count).equals(keypath(Array<String>, .count))
// BinaryPredicate<Array<Int>, Array<String>, Bool>
if areSameLength.evaluate([1,2,3], [“a”, “b”, “c”]) {
    print(“same lengths”)
}

--
Brent Royal-Gordon
Architechies

Apologies, been working with lots of generic closures today and the ‘Result’ just came out instinctively. Of course the Result of a predicate is a Boolean…

Still, it’s useful to think about what some more advanced use-cases of keypaths might look like with this proposal. As far as syntax is concerned, operators may ultimately be the best way to make long predicate combinations cleaner.

extension UnaryPredicate {
    static func &&<P>(left: UnaryPredicate<P>, right: UnaryPredicate<P>) -> UnaryPredicate<P> {
        return UnaryPredicate { self($0) && other($0) }
    }
}

let isHealthyPuppy = keypath(Pet, .type).equals(.dog) && keypath(Pet, .age).isLessThan(12) && keypath(Pet, .health).equals(.good)

vs

let isHealthyPuppy = keypath(Pet, .type).equals(.dog).and(keypath(Pet, .age).isLessThan(12)).and(keypath(Pet, .health).equals(.good))

- Karl

···

On 2 Apr 2017, at 02:56, Karl Wagner <karl.swift@springsup.com> wrote:

struct UnaryPredicate<Parameter> {
    let evaluate: (Parameter) -> Bool
}
struct BinaryPredicate<Left, Right> {
    let evaluate: (Left, Right) -> Bool
}

extension KeyPath where Value: Equatable {
    func equals(_ value: Value) -> UnaryPredicate<Root> {
        return UnaryPredicate { $0[keyPath: self] == value }
    }
    func equals<KP: KeyPath>(_ other: KP) -> BinaryPredicate<Root, KP.Root> where KP.Value == Value {
        return BinaryPredicate { $0[keyPath: self] == $1[keyPath: other] }
    }
}

let isDog = keypath(Pet, .type).equals(.dog) // UnaryPredicate<Pet>
if isDog.evaluate(somePet) {
    print(“It’s a dog”)
}

let areSameLength = keypath(Array<Int>, .count).equals(keypath(Array<String>, .count))
// BinaryPredicate<Array<Int>, Array<String>>
if areSameLength.evaluate([1,2,3], [“a”, “b”, “c”]) {
    print(“same lengths”)
}

Grammatically, it's a series of postfix expression components, including property accesses `.foo`, subscripts `[bar]`, and optional operators `?` and `!`. The grammar is more uniform requiring the dot.

-Joe

···

On Apr 3, 2017, at 7:24 AM, Ricardo Parada via swift-evolution <swift-evolution@swift.org> wrote:

By the way, does anybody know why the proposed syntax require a leading period for the key path?

keypath(Family, .pets.first)

Thanks Brent. Was that real code? :-)

If it was, I did not realize it was possible to do something like that in Swift. That seems very cool, attractive and powerful. It certainly would look much better with the sugary syntax.

I can imagine building a collection of sort orderings like this for example.

// Order by lastName asc, firstName asc
let orderings = Person.lastName.asc().then(.firstName.asc())

Don't know if it would be possible but I think it would be nice to be able to do it. The asc() method would create an ascending sort ordering and would establish Root as Person. I wonder if it would be possible to have the then() know that Root is person so that its argument can be a key path relative to Person. Or maybe this can be expressed differently if this is not the right way.

Anyways, the sugary syntax looks very attractive IMHO.

This is what my Java code looks like for something like that:

NSArray<EOSortOrdering> orderings = Person.LAST_NAME.asc().then(Person.FIRST_NAME.asc());

or this:

NSArray<EOSortOrdering> orderings =
    new NSArray<>(
        Person.LAST_NAME.asc(),
        Person.FIRST_NAME.asc()
    );

···

On Apr 1, 2017, at 10:24 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

On Apr 1, 2017, at 5:56 PM, Karl Wagner via swift-evolution <swift-evolution@swift.org> wrote:

let isPuppyQualifier = Pet.type.equals(.dog).and(Pet.age.lessThan(12))
let familyQualifier = Family.pets.hasAtLeastOne(satisfying: isPuppyQualifier)
let familiesWithPuppies = Family.fetch(editingContext, familyQualifier)

For those unfamiliar with EOF, the editingContext in the code above is an EOEditingContext which is analogous to NSManagedObjectContext.

Theoretically, you could do something like that with this proposal...

struct UnaryPredicate<Parameter, Result> {
    let evaluate: (Parameter) -> Result
}
struct BinaryPredicate<Left, Right, Result> {
    let evaluate: (Left, Right) -> Result
}

extension KeyPath where Value: Equatable {
    func equals(_ value: Value) -> UnaryPredicate<Root, Bool> {
        return UnaryPredicate { $0[keyPath: self] == value }
    }
    func equals<KP: KeyPath>(_ other: KP) -> BinaryPredicate<Root, KP.Root, Bool> where KP.Value == Value {
        return BinaryPredicate { $0[keyPath: self] == $1[keyPath: other] }
    }
}

let isDog = keypath(Pet, .type).equals(.dog) // UnaryPredicate<Pet, Bool>
if isDog.evaluate(somePet) {
    print(“It’s a dog”)
}

let areSameLength = keypath(Array<Int>, .count).equals(keypath(Array<String>, .count))
// BinaryPredicate<Array<Int>, Array<String>, Bool>
if areSameLength.evaluate([1,2,3], [“a”, “b”, “c”]) {
    print(“same lengths”)
}

You guys aren't thinking big enough.

  // This implementation is closure-based because it's too complex for an email even *with*
  // generics system upgrades. Without type system upgrades, things get *really* complicated,
  // though probably still tractable.
  typealias Predicate<Element> = (Element) -> Bool
  
  func == <Root, Value: Equatable> (lhs: KeyPath<Root, Value>, rhs: Value) -> Predicate<Root> {
    return { $0[keyPath: lhs] == rhs }
  }
  
  func < <Root, Value: Comparable> (lhs: KeyPath<Root, Value>, rhs: Value) -> Predicate<Root> {
    return { $0[keyPath: lhs] < rhs }
  }

  func && <Root>(lhs: Predicate<Root>, rhs: Predicate<Root>) -> Predicate<Root> {
    return { lhs($0) && rhs($0) }
  }

  extension KeyPath where Value: Collection {
    func contains(where predicate: Predicate<Value.Element>) -> Predicate<Value> {
      return { $0.contains(where: predicate) }
    }
  }

That gives you:

  let isPuppyQualifier = keypath(Pet, .type) == .dog && keypath(Pet, .age) < 12
  let familyQualifier = keypath(Family, .pets).contains(where: isPuppyQualifier)
  let familiesWithPuppies = Family.fetch(editingContext, familyQualifier)

Or, in one line with a more sugary syntax:

  let familiesWithPuppies = Family.fetch(editingContext, (.pets).contains(where: .type == .dog && .age < 12))

--
Brent Royal-Gordon
Architechies