I would suggest a keypath using ~ which is concise and clearer to identify.
let myPath = Person~friends[0].name
···
On Sunday, March 19, 2017, Jonathan Hull via swift-evolution < swift-evolution@swift.org> wrote:
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,
JonOn Mar 17, 2017, at 10:04 AM, Michael LeHew via swift-evolution < > swift-evolution@swift.org > <javascript:_e(%7B%7D,'cvml','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,
-MichaelSmart 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.
MotivationWe Can Do Better than StringOn 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 platformsUse/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 KeyPathsWe would also like to support being able to use *Key Paths* to access
into collections, which is not currently possible.
Proposed solutionWe 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:
Swiftstruct 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 contextlet 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 jediDetailed designCore 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 TypeAnyKeyPath 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.
Swiftclass 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.Typestatic 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:
Swiftclass 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.
Swiftpublic 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.
Swiftclass 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.
Swiftextension 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
Swiftperson[.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 PathsForming 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.
PerformanceThe performance of interacting with a property via KeyPaths should be
close to the cost of calling the property directly.
Source compatibilityThis change is additive and there should no affect on existing source.
Effect on ABI stabilityThis 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 resilienceThis should not significantly impact API resilience, as it merely provides
a new mechanism for operating on existing APIs.
Alternatives consideredMore FeaturesVarious drafts of this proposal have included additional features
(decomposable key paths, prefix comparisons, support for custom