Sent off-list by mistake: Nice proposal. I have a few comments inline:
> On 17 Mar 2017, at 18:04, 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. > What metadata is attached? How is it accessed? What future features are you thinking about? > > 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) > How different would the design be with generalized existentials? Are they plans to migrate to that design once we do get 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 Perhaps I'm missing something, but what does that syntax bring compared to person.name? I see quite a few examples of the key paths being used literally in the subscript syntax but fail to see the usefulness of doing that. Can you give use cases? > 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. > The proposal is elegant but I don't yet see the use cases this feature would allow. Could you give us examples of how the Standard Library or a user's code could benefit from it? I mean, KVC is really powerful in Objective-C it comes hand in hand with KVO. Do you envision similar observation libraries be built on top of key paths? > _______________________________________________ > > 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