[swift-evolution] Smart KeyPaths

Matthew Johnson matthew at anandabits.com
Fri Mar 17 21:29:11 CDT 2017


> On Mar 17, 2017, at 5:38 PM, Joe Groff via swift-evolution <swift-evolution at swift.org> wrote:
> 
> 
>> On Mar 17, 2017, at 12:34 PM, David Hart via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>> 
>> 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 at swift.org <mailto: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.
>>> 
>> What metadata is attached? How is it accessed? What future features are you thinking about?
> 
> To begin with, you'd have limited ability to stringify a key path. Eventually we'd like to support other reflectiony things, including:
> 
> - Asking for the primary key paths a type supports
> - Asking for a key by name or index
> - Breaking a key path down by components
> 
> I also see key path objects as a good way of eventually addressing some of the design problems we ran up against with property behaviors (https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md <https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md> from last year), including the problem of what exactly a property behavior declaration *is* (a type? a protocol? a function-like thing? something completely new?), and the problem of handling "out-of-band" operations on a property beyond getting and setting, such as clearing a cached lazy value, registering for notifications on an observable property, and so on. I think it would be natural to express property behaviors as a user-defined key path type; the key path type can provide the get/set logic for the property as well as any other interesting operations the property supports. This answers the questions of both what behaviors look like (they're just types that conform to KeyPath) and how they extend properties with new actions (they're just methods of the key path value) fairly nicely.

Would these user-defined key path types be unbound like the ones described in the current proposal?  That would be really cool.  

I am thinking through how this would impact the design of a library I’m working on.  It is a use case where I would want a single unbound instance per property rather than an instance of the key path (and its storage) for each instance of the type.  Storage of the property value itself would not be affected by the behavior.

I would use reflection to discover the key paths of a type that my library recognizes and use them to interact with the property.  I would have the user initialize the custom key path for a property with a key path into a related type where both have the same value type.  The library would use the key paths to synchronize data between the related instances.  There is no need to change the per-instance storage or access (the library would not rely on intercepting specific accesses to the property to synchronize the data).  It would only be used as a powerful tool for working with arbitrary types.

>>> 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?
> 
> The value comes from `.name` being a separate value from `person`. In the same way that closures let you abstract over functions and methods as plain old values independent of their original context, keypaths should let you do the same with a property.
> 
> -Joe
> _______________________________________________
> 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/20170317/bf65a02a/attachment-0001.html>


More information about the swift-evolution mailing list