[swift-evolution] Smart KeyPaths

Ricardo Parada rparada at mac.com
Sun Mar 19 10:16:14 CDT 2017


It looks awesome.  

I don’t understand the details yet but I always felt it would be super cool if swift allowed you to express key paths elegantly which seems what this is.  

I would love to be able to create what used to be called EOQualifier in WebObjects/Enterprise Objects Framework (i.e. NSPredicate in CoreData I think) using a better syntax.  Same for EOSortOrdering, equivalent to NSSortDescriptor.  For example:

let predicate = Person.lastName.like(“Para*").and(Person.birthDate.greaterThan(aDate))

Fetch objects from the database into a managed object context like this:

let people = context.fetch(Person.self, predicate: predicate)		// people is inferred as Array<Person>

Or perhaps filter elements from an array like this:

let matches = people.filtered(predicate)				// matches is inferred as Array<Person>

Or create sort orderings like this:

let orderings = Person.age.desc().then(Person.lastName.asc().then(Person.lastName.asc()))   // orderings is Array<SortOrdering>

And sort like this:

let sortedPeople = people.sorted(orderings)		// sortedPeople is Array<Person>

And predicates for to-many relationships, for example, if we had a Family class and a Pet class and a pets to-many relationship Family <—>> Pet, then building a predicate like this would be cool:

// Families with at least one pet
let predicate = Family.pets.isNotEmpty()

// Families with no pets
let predicate = Family.pets.isEmpty()

// Families with a cat or dog
let predicate = Family.pets.hasAtLeastOneObjectSatisfying(Pet.type.equals(.dog))

// Families with a puppy
let predicate = Family.pets.hasAtLeastOneObjectSatisfying(Pet.type.equals(.dog).and(Pet.ageInMonths.lessThan(12))

In SQL these translate to an EXISTS qualifier. For example, the last predicate would translate to something like this in SQL:

SELECT t0.family_id, …
FROM family t0
WHERE 
    EXISTS (
        select p.pet_id
        from pet p
        where p.family_id = t0.family_id
            and p.pet_type = ‘dog’
            and p.age_in_months < 12
    )


Would these Star KeyPaths enable this sort of expressiveness?

Or creating ad hoc queries like this:

// records is Array<Dictionary<String,Object>>
let records = Query()
    .select (Claim.provider, Claim.sumExpectedAmount)  // Claim.sumExpectedAmount is a derived non-class property defined as "SUM(expectedAmount)"
    .from (Claim.entityName)	// Claim.entityName is a static property for string “Claim”
    .where (Claim.userGroup.equals(aUserGroup))
    .groupBy (Claim.provider)
    .having (Claim.sumExpectedAmount.greaterThan(1000.0))
    .orderBy (Claim.sumExpectedAmount.asc())
    .fetch (editingContext)
    
// Or to fetch into a custom class

// objects is Array<Foo>
let objects = Query()
    .select (Claim.provider, Claim.sumExpectedAmount)  // Claim.sumExpectedAmount is a derived non-class property defined as "SUM(expectedAmount)"
    .from (Claim.entityName)	// Claim.entityName is a static property for string “Claim”
    .where (Claim.userGroup.equals(aUserGroup))
    .groupBy (Claim.provider)
    .having (Claim.sumExpectedAmount.greaterThan(1000.0))
    .orderBy (Claim.sumExpectedAmount.asc())
    .fetch (context) {
        Foo(context: ec, data: row)
    }

Then print the results like this:

for obj in objects {
    Print(“Health provider: \(obj.provider?.fullName), Total Expected: \(obj.sumExpectedAmount)”)
}

Where Foo would be a custom class to hold the results and provide some type checking on the getters for the data fetched:

class Foo {
    var data : [String: Any]

    init(context: EditingContext, data row: [String: Any]) {
       data = row
    }


    var provider : Provider? {
        get {
           return data[“provider”] as? Provider
        }
    }

    var provider : Double? {
        get {
           return data[“sumExpectedAmount”] as? Double
        }
    }
}


By the way I do this kind of stuff with WebObjects and Project Wonder.  It’s just that the key paths are static variables defined in the class in ALL CAPS, i.e. Person.FIRST_NAME.asc() to get an EOSortOrdering for ascending firstName.  They also look not as elegant for key paths, i.e. Claim.PROVIDER.dot(Provider.LAST_NAME).like(“Para*”) for an EOQualifier.  This is all provided by a class named ERXKey.

And for ad hoc queries I use ERXQuery which I recently created a pull request to add to project Wonder.  It’s in my repository rparada/wonder on GitHub in the erxquery branch.




> On Mar 17, 2017, at 1:04 PM, Michael LeHew via swift-evolution <swift-evolution at swift.org> wrote:
> 
> Hi friendly swift-evolution folks,
> 
> The Foundation and Swift team  would like for you to consider the following proposal:
> 
> Many thanks,
> -Michael
> 
> Smart KeyPaths: Better Key-Value Coding for Swift
> Proposal: SE-NNNN
> Authors: David Smith <https://github.com/Catfish-Man>, Michael LeHew <https://github.com/mlehew>, Joe Groff <https://github.com/jckarter>
> Review Manager: TBD
> Status: Awaiting Review
> Associated PRs:
> #644 <https://github.com/apple/swift-evolution/pull/644>
> Introduction
> We propose a family of concrete Key Path types that represent uninvoked references to properties that can be composed to form paths through many values and directly get/set their underlying values.
> 
> Motivation
> We Can Do Better than String
> 
> On Darwin platforms Swift's existing #keyPath() syntax provides a convenient way to safely refer to properties. Unfortunately, once validated, the expression becomes a String which has a number of important limitations:
> 
> Loss of type information (requiring awkward Any APIs)
> Unnecessarily slow to parse
> Only applicable to NSObjects
> Limited to Darwin platforms
> Use/Mention Distinctions
> 
> While methods can be referred to without invoking them (let x = foo.bar instead of  let x = foo.bar()), this is not currently possible for properties and subscripts.
> 
> Making indirect references to a properties' concrete types also lets us expose metadata about the property, and in the future additional behaviors.
> 
> More Expressive KeyPaths
> 
> We would also like to support being able to use Key Paths to access into collections, which is not currently possible.
> 
> Proposed solution
> We propose introducing a new expression akin to Type.method, but for properties and subscripts. These property reference expressions produce KeyPath objects, rather than Strings. KeyPaths are a family of generic classes (structs and protocols here would be ideal, but requires generalized existentials) which encapsulate a property reference or chain of property references, including the type, mutability, property name(s), and ability to set/get values.
> 
> Here's a sample of it in use:
> 
> Swift
> struct Person {
>     var name: String
>     var friends: [Person]
>     var bestFriend: Person?
> }
> 
> var han = Person(name: "Han Solo", friends: [])
> var luke = Person(name: "Luke Skywalker", friends: [han])
> 
> let firstFriendsNameKeyPath = Person.friends[0].name
> 
> let firstFriend = luke[path] // han
> 
> // or equivalently, with type inferred from context
> let firstFriendName = luke[.friends[0].name]
> 
> // rename Luke's first friend
> luke[firstFriendsNameKeyPath] = "A Disreputable Smuggler"
> 
> let bestFriendsName = luke[.bestFriend]?.name  // nil, if he is the last jedi
> Detailed design
> Core KeyPath Types
> 
> KeyPaths are a hierarchy of progressively more specific classes, based on whether we have prior knowledge of the path through the object graph we wish to traverse. 
> 
> Unknown Path / Unknown Root Type
> 
> AnyKeyPath is fully type-erased, referring to 'any route' through an object/value graph for 'any root'. Because of type-erasure many operations can fail at runtime and are thus nillable. 
> 
> Swift
> class AnyKeyPath: CustomDebugStringConvertible, Hashable {
>     // MARK - Composition
>     // Returns nil if path.rootType != self.valueType
>     func appending(path: AnyKeyPath) -> AnyKeyPath?
>     
>     // MARK - Runtime Information        
>     class var rootType: Any.Type
>     class var valueType: Any.Type
>     
>     static func == (lhs: AnyKeyPath, rhs: AnyKeyPath) -> Bool
>     var hashValue: Int
> }
> Unknown Path / Known Root Type
> 
> If we know a little more type information (what kind of thing the key path is relative to), then we can use PartialKeyPath <Root>, which refers to an 'any route' from a known root:
> 
> Swift
> class PartialKeyPath<Root>: AnyKeyPath {
>     // MARK - Composition
>     // Returns nil if Value != self.valueType
>     func appending(path: AnyKeyPath) -> PartialKeyPath<Root>?
>     func appending<Value, AppendedValue>(path: KeyPath<Value, AppendedValue>) -> KeyPath<Root, AppendedValue>?
>     func appending<Value, AppendedValue>(path: ReferenceKeyPath<Value, AppendedValue>) -> ReferenceKeyPath<Root, AppendedValue>?
> }
> Known Path / Known Root Type
> 
> When we know both what the path is relative to and what it refers to, we can use KeyPath. Thanks to the knowledge of the Root and Value types, all of the failable operations lose their Optional. 
> 
> Swift
> public class KeyPath<Root, Value>: PartialKeyPath<Root> {
>     // MARK - Composition
>     func appending<AppendedValue>(path: KeyPath<Value, AppendedValue>) -> KeyPath<Root, AppendedValue>
>     func appending<AppendedValue>(path: WritableKeyPath<Value, AppendedValue>) -> Self
>     func appending<AppendedValue>(path: ReferenceWritableKeyPath<Value, AppendedValue>) -> ReferenceWritableKeyPath<Root, AppendedValue>
> }
> Value/Reference Mutation Semantics Mutation
> 
> Finally, we have a pair of subclasses encapsulating value/reference mutation semantics. These have to be distinct because mutating a copy of a value is not very useful, so we need to mutate an inout value.
> 
> Swift
> class WritableKeyPath<Root, Value>: KeyPath<Root, Value> {
>     // MARK - Composition
>     func appending<AppendedPathValue>(path: WritableKeyPath<Value, AppendedPathValue>) -> WritableKeyPath<Root, AppendedPathValue>
> }
> 
> class ReferenceWritableKeyPath<Root, Value>: WritableKeyPath<Root, Value> {
>     override func appending<AppendedPathValue>(path: WritableKeyPath<Value, AppendedPathValue>) -> ReferenceWritableKeyPath<Root, AppendedPathValue>
> }
> Access and Mutation Through KeyPaths
> 
> To get or set values for a given root and key path we effectively add the following subscripts to all Swift types. 
> 
> Swift
> extension Any {
>     subscript(path: AnyKeyPath) -> Any? { get }
>     subscript<Root: Self>(path: PartialKeyPath<Root>) -> Any { get }
>     subscript<Root: Self, Value>(path: KeyPath<Root, Value>) -> Value { get }
>     subscript<Root: Self, Value>(path: WritableKeyPath<Root, Value>) -> Value { set, get }
> }
> This allows for code like
> 
> Swift
> person[.name] // Self.type is inferred
> which is both appealingly readable, and doesn't require read-modify-write copies (subscripts access self inout). Conflicts with existing subscripts are avoided by using generic subscripts to specifically only accept key paths with a Root of the type in question.
> 
> Referencing Key Paths
> 
> Forming a KeyPath borrows from the same syntax used to reference methods and initializers,Type.instanceMethod only now working for properties and collections. Optionals are handled via optional-chaining. Multiply dotted expressions are allowed as well, and work just as if they were composed via the appending methods on KeyPath.
> 
> There is no change or interaction with the #keyPath() syntax introduced in Swift 3. 
> 
> Performance
> 
> The performance of interacting with a property via KeyPaths should be close to the cost of calling the property directly.
> 
> Source compatibility
> This change is additive and there should no affect on existing source. 
> 
> Effect on ABI stability
> This feature adds the following requirements to ABI stability: 
> 
> mechanism to access key paths of public properties
> We think a protocol-based design would be preferable once the language has sufficient support for generalized existentials to make that ergonomic. By keeping the class hierarchy closed and the concrete implementations private to the implementation it should be tractable to provide compatibility with an open protocol-based design in the future.
> 
> Effect on API resilience
> This should not significantly impact API resilience, as it merely provides a new mechanism for operating on existing APIs.
> 
> Alternatives considered
> More Features
> 
> Various drafts of this proposal have included additional features (decomposable key paths, prefix comparisons, support for custom KeyPath subclasses, creating a KeyPath from a String at runtime, KeyPaths conforming to Codable, bound key paths as a concrete type, etc.). We anticipate approaching these enhancements additively once the core KeyPath functionality is in place. 
> 
> Spelling
> 
> We also explored many different spellings, each with different strengths. We have chosen the current syntax due to the balance with existing function type references.
> 
> Current	#keyPath	Lisp-style
> Person.friends[0].name	#keyPath(Person, .friends[0].name)	`Person.friend.name
> luke[.friends[0].name]	#keyPath(luke, .friends[0].name)	luke`.friends[0].name
> luke.friends[0][.name]	#keyPath(luke.friends[0], .name)	luke.friends[0]`.name
> While the crispness is very appealing, the spelling of the 'escape' character was hard to agree upon (along with the fact that it requires parentheses to reduce ambiguity).  #keyPath was very specific, but verbose especially when composing multiple key paths together.
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170319/8d255c94/attachment-0001.html>


More information about the swift-evolution mailing list