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 
> <[email protected]> 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
> [email protected]
> https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to