This looks fantastic!

The one worry I would have, as others have brought up, is confusion when there 
are static properties with the same name.  Maybe we could have a special static 
property called ‘keyPath’ or ‘path’ that would be reserved, and anything that 
followed would be a key path.

So instead of:

        let myPath = Person.friends[0].name

you would have:

        let myPath = Person.keyPath.friends[0].name


Or if we want to choose a sentinel, I would nominate ‘$’:

        let myPath = $Person.friends[0].name

Thanks,
Jon


> On Mar 17, 2017, at 10:04 AM, Michael LeHew via swift-evolution 
> <swift-evolution@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@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to