[swift-evolution] Smart KeyPaths

Colin Barrett colin at springsandstruts.com
Sat Mar 18 15:39:14 CDT 2017


It's so nice that this is finally seeing the light of day :) Great work
everyone!

Re: subscripts, it's definitely a great solution for "the Swift we have
now", but I'm not sure in "the Swift we'll have in a few years." If, for
instance, someday we're able to return inouts (or really just lvalues in
general), we'd be able to do a lot of this with regular functions and some
combinators.

Count me as a +1 for the Lisp-style syntax. If ` isn't to people's liking,
I wonder about @. I think both "luke @  .friends[0].name" and "
.friends[0].name @ luke" read quiet nicely, and gives the idea of a key
path as a kind of addressing scheme.

-Colin

P.S. Has anyone thought about writing an HTTP + JSON DSL with these?

On Fri, Mar 17, 2017 at 1:04 PM Michael LeHew via swift-evolution <
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.
> MotivationWe 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 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 jedi
>
> Detailed 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 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 consideredMore 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
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170318/8de5ce54/attachment.html>


More information about the swift-evolution mailing list