[Pitch] Improving KeyPath

Hey folks!

Recently I’ve been working on a small library which leverages the Swift 4 Codable protocol
and KeyPaths to provide a Swift-y interface to CoreData. (It maps back and forth between
native, immutable Swift structs and NSManagedObjects). In doing so I found a couple of
frustrating limitations to the KeyPath API. Firstly, KeyPath does not provide the name of the
property on the type it indexes. For example, if I have a struct:

struct Person {
    let firstName: String
    let lastName: String
}

let keyPath = \Person.firstName

But once I have a keyPath, I can’t actually figure out what property it accesses.
So, I wind up having to make a wrapper:

struct Attribute {
    let keyPath: AnyKeyPath
    let propertyName: String
}

let firstNameAttribute = Attribute(keyPath: \Person.firstName, propertyName: “firstName”)

This forces me to write out the property name myself as a string which is very error prone.
All I want is to be able to access:

keyPath.propertyName // “firstName”

It would also be nice if we provided the full path as a string as well:

keyPath.fullPath // “Person.firstName"

Also, if I want to get all of the attributes from a given Swift type, my options are to try to hack
something together with Mirrors, or forcing the type to declare a function / computed property
returning an array of all of its key path / property name pairings. I would really like to be able to
retrieve a type-erased array of any type’s key paths with:

let person = Person(firstName: “John”, lastName: “Doe”)
let keyPaths = Person.keyPaths
let firstNameKeyPath = keyPaths.first { $0.propertyName = “firstName” } as! KeyPath<Person, String>
let firstName = person[keypath: firstNameKeyPath] // “John"

And finally, without straying too far into Objective-C land, it would be nice if we could initialize key paths
with a throwing initializer.

let keyPath = try Person.keyPath(“firstName”) // KeyPath<Person, String> type erased to AnyKeyPath
let keyPath = AnyKeyPath(“Person.firstName”)

Let me know what you think about any / all of these suggestions!

Thanks,
Logan

2 Likes

These would all be great additional features to eventually add to key paths. I think reflection mechanisms centered on key paths like what you describe would be a superior replacement for most of what Mirror attempts to provide.

-Joe

···

On Aug 23, 2017, at 11:18 PM, Logan Shire via swift-evolution <swift-evolution@swift.org> wrote:

Hey folks!

Recently I’ve been working on a small library which leverages the Swift 4 Codable protocol
and KeyPaths to provide a Swift-y interface to CoreData. (It maps back and forth between
native, immutable Swift structs and NSManagedObjects). In doing so I found a couple of
frustrating limitations to the KeyPath API. Firstly, KeyPath does not provide the name of the
property on the type it indexes. For example, if I have a struct:

struct Person {
   let firstName: String
   let lastName: String
}

let keyPath = \Person.firstName

But once I have a keyPath, I can’t actually figure out what property it accesses.
So, I wind up having to make a wrapper:

struct Attribute {
   let keyPath: AnyKeyPath
   let propertyName: String
}

let firstNameAttribute = Attribute(keyPath: \Person.firstName, propertyName: “firstName”)

This forces me to write out the property name myself as a string which is very error prone.
All I want is to be able to access:

keyPath.propertyName // “firstName”

It would also be nice if we provided the full path as a string as well:

keyPath.fullPath // “Person.firstName"

Also, if I want to get all of the attributes from a given Swift type, my options are to try to hack
something together with Mirrors, or forcing the type to declare a function / computed property
returning an array of all of its key path / property name pairings. I would really like to be able to
retrieve a type-erased array of any type’s key paths with:

let person = Person(firstName: “John”, lastName: “Doe”)
let keyPaths = Person.keyPaths
let firstNameKeyPath = keyPaths.first { $0.propertyName = “firstName” } as! KeyPath<Person, String>
let firstName = person[keypath: firstNameKeyPath] // “John"

And finally, without straying too far into Objective-C land, it would be nice if we could initialize key paths
with a throwing initializer.

let keyPath = try Person.keyPath(“firstName”) // KeyPath<Person, String> type erased to AnyKeyPath
let keyPath = AnyKeyPath(“Person.firstName”)

Let me know what you think about any / all of these suggestions!

3 Likes

I concur with Logan’s idea here on the general points, but let me add a bit more.

Here are some KeyPathy things I’d like to see in a future Swift:

/// A set of PartialKeyPath<Type> guaranteed as:
/// (a) the entire set of keypaths for a type; and
/// (b) accessible given the current scope
Type.allKeyPaths() throws -> [PartialKeyPath<Type>]

/// A set of PartialKeyPath<Type> guaranteed as:
/// (a) sufficient for initialization of an instance; and
/// (b) accessible given the current scope
Type.sufficientPartialKeyPaths() throws -> [PartialKeyPath<Type>]

class Property<KeyPath<RootType,ValueType>> {
    let keyPath: KeyPath<RootType, ValueType>
    let value: ValueType
}

Type.init(with properties: [PartialProperty<Type>])

Type.init(copy: Type, overwriting properties: [PartialProperty<Type>])

The idea is a type can provide you a set of PartialKeyPath<Type> that is guaranteed as sufficient for initialization of an instance of the type, as long as the current scope lets you access it.

What would also be nice:

/// A set of PartialKeyPath<Type> guaranteed as
/// (a) the entire set of writable keypaths of Type; and
/// (b) accessible given the current scope
AllWritableKeyPaths<Type, Element>

(etc.) :smiley:

Note: in Swift 3.2/4, (of course), AnyKeyPaths and PartialKeyPaths<T> can already be downcast to more specific types like KeyPath<T, E>, WritableKeyPath<T, E>, etc., but only if you already know what T and E are at compile time (i.e. they are not generic).

I have found some bugs though; iterating through arrays of AnyKeyPath using “where” statements to limit the types is a buggy and unpredictable affair (I believe “filter(into:)” works best).

E.g.:

extension Array where Element == AnyKeyPath {
    func partialKeyPaths<T>() -> [T] {
        return self.filter(into: [PartialKeyPath<T>]())
        { result, keyPath in
            if let k = keyPath as? PartialKeyPath<T> {
                result.append(k)
            }
        }
    }
}

To what end?

What we sorely lack in Swift is a way to (failably) init an object from a set of keypaths and values without tons of boilerplate and/or resorting to using string keys etc.

Worse, right now there is no way to make a copy of an object/struct while mutating it only at one or two keypaths without writing yet more boilerplatey init methods.

Heck, right now, keypaths can be used for initializing neither immutable instance constants, nor mutable instance variables that lack default initializers. E.g.: self[keyPath: \Foo.bar] = “baz” fails to compile inside an init method, because the property is not initialized yet. Gee.

Towards Type-Safe Instance Composition Patterns:

In a type-safe language we can’t have ECMA6-style destructuring (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)... and please don’t accuse me of wanting it.

All I want is some sugar that makes the Swift compiler infer more different kinds of convenience init methods. Something like:

struct Foo {
    let bar: String
    let baz: String
    let leadScientist: QuantumPhysicist
    let labTech: TeleporterTester
}

let fooMarch = Foo(bar: “asdf”, baz: “qwer”, leadScientist: QuantumPhysicist(“Alice”), labTech: TeleporterTester(“Bob”))

let fooApril = Foo(copy: fooMarch, overwriting: Property(\.labTech, TeleporterTester(“Charlie”))

... with “overwriting” taking 0 or more variadic arguments.

This allows easily, concisely composing an immutable instance of a type out of various components of other immutable instances of a type. I think this is an extremely powerful pattern, and many times I wish that I had it.

In the absence of this, devs are prone to just use mutable instance vars instead of using immutable instance constants, just so they don’t have to do a whole member-wise initializer every time they want to just change one property.

Just my $0.02.

If there is already a way to use these things like that, then I want to know it.

As for “why would this really be useful”, “what are the real-world benefits”, etc. ... I feel like if you really have to ask this, then it’s not because you actually cannot see the obvious benefits—it’s because you hate America.

~ Jon Gilbert

···

On Aug 23, 2017, at 23:19, Logan Shire via swift-evolution <swift-evolution@swift.org> wrote:

Hey folks!

Recently I’ve been working on a small library which leverages the Swift 4 Codable protocol
and KeyPaths to provide a Swift-y interface to CoreData. (It maps back and forth between
native, immutable Swift structs and NSManagedObjects). In doing so I found a couple of
frustrating limitations to the KeyPath API. Firstly, KeyPath does not provide the name of the
property on the type it indexes. For example, if I have a struct:

struct Person {
   let firstName: String
   let lastName: String
}

let keyPath = \Person.firstName

But once I have a keyPath, I can’t actually figure out what property it accesses.
So, I wind up having to make a wrapper:

struct Attribute {
   let keyPath: AnyKeyPath
   let propertyName: String
}

let firstNameAttribute = Attribute(keyPath: \Person.firstName, propertyName: “firstName”)

This forces me to write out the property name myself as a string which is very error prone.
All I want is to be able to access:

keyPath.propertyName // “firstName”

It would also be nice if we provided the full path as a string as well:

keyPath.fullPath // “Person.firstName"

Also, if I want to get all of the attributes from a given Swift type, my options are to try to hack
something together with Mirrors, or forcing the type to declare a function / computed property
returning an array of all of its key path / property name pairings. I would really like to be able to
retrieve a type-erased array of any type’s key paths with:

let person = Person(firstName: “John”, lastName: “Doe”)
let keyPaths = Person.keyPaths
let firstNameKeyPath = keyPaths.first { $0.propertyName = “firstName” } as! KeyPath<Person, String>
let firstName = person[keypath: firstNameKeyPath] // “John"

And finally, without straying too far into Objective-C land, it would be nice if we could initialize key paths
with a throwing initializer.

let keyPath = try Person.keyPath(“firstName”) // KeyPath<Person, String> type erased to AnyKeyPath
let keyPath = AnyKeyPath(“Person.firstName”)

Let me know what you think about any / all of these suggestions!

Thanks,
Logan

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

Reflection mechanisms in general would let one make things like Codable and Keypaths simply a library capability/protocol extension rather than the special case one trick pony it is now.

More than any other feature discussed, full access to meta data such as memory layouts is the thing I most miss from Objective C.

Given reflection/introspection, you can build KVC, Codable, and any number of other clever features yourself if you are so inclined.

Oh, and traits - want traits for composability.

···

On Aug 25, 2017, at 11:43 AM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

These would all be great additional features to eventually add to key paths. I think reflection mechanisms centered on key paths like what you describe would be a superior replacement for most of what Mirror attempts to provide.

Reflection mechanisms in general would let one make things like Codable and Keypaths simply a library capability/protocol extension rather than the special case one trick pony it is now.

More than any other feature discussed, full access to meta data such as memory layouts is the thing I most miss from Objective C.

Given reflection/introspection, you can build KVC, Codable, and any number of other clever features yourself if you are so inclined.

Runtime information about type layouts exists in the Swift runtime, and there are many ways we can choose to expose it to programmers. There needs to be some stable runtime-provided interface at the bottom, and KeyPath has some advantages over ObjC-style string-based KVC as that foundation—they're strongly typed, and they don't need to depend on string descriptions of everything being embedded in the binary, which might be undesirable for secrecy reasons. We could allow a type to vend a collection of key paths to all of its fields without having to fully expose source-level details of what those fields are named and how they're laid out, which has been a shortcoming of ObjC's approach for a lot of secrecy-sensitive clients.

Oh, and traits - want traits for composability.

What do you mean exactly by traits? That's an overloaded term.

-Joe

···

On Aug 25, 2017, at 1:27 PM, Eagle Offshore <eagleoffshore@me.com> wrote:

How would you feel about wrapping the existing functions
on _KVOKeyPathBridgeMachinery:

@nonobjc fileprivate static func _bridgeKeyPath(_ keyPath:AnyKeyPath) ->
String
@nonobjc fileprivate static func _bridgeKeyPath(_ keyPath:String?) ->
AnyKeyPath?

In extensions on String and AnyKeyPath respectively to instantiate strings
from KeyPaths and KeyPaths from Strings?

https://github.com/apple/swift/blob/c5ff1f2cac8da6a14330f4b033b94c7c926d2126/stdlib/public/SDK/Foundation/NSObject.swift#L84

···

On Fri, Aug 25, 2017 at 11:43 AM Joe Groff <jgroff@apple.com> wrote:

> On Aug 23, 2017, at 11:18 PM, Logan Shire via swift-evolution < > swift-evolution@swift.org> wrote:
>
> Hey folks!
>
> Recently I’ve been working on a small library which leverages the Swift
4 Codable protocol
> and KeyPaths to provide a Swift-y interface to CoreData. (It maps back
and forth between
> native, immutable Swift structs and NSManagedObjects). In doing so I
found a couple of
> frustrating limitations to the KeyPath API. Firstly, KeyPath does not
provide the name of the
> property on the type it indexes. For example, if I have a struct:
>
>
> struct Person {
> let firstName: String
> let lastName: String
> }
>
> let keyPath = \Person.firstName
>
>
> But once I have a keyPath, I can’t actually figure out what property it
accesses.
> So, I wind up having to make a wrapper:
>
>
> struct Attribute {
> let keyPath: AnyKeyPath
> let propertyName: String
> }
>
> let firstNameAttribute = Attribute(keyPath: \Person.firstName,
propertyName: “firstName”)
>
>
> This forces me to write out the property name myself as a string which
is very error prone.
> All I want is to be able to access:
>
>
> keyPath.propertyName // “firstName”
>
>
> It would also be nice if we provided the full path as a string as well:
>
>
> keyPath.fullPath // “Person.firstName"
>
>
> Also, if I want to get all of the attributes from a given Swift type, my
options are to try to hack
> something together with Mirrors, or forcing the type to declare a
function / computed property
> returning an array of all of its key path / property name pairings. I
would really like to be able to
> retrieve a type-erased array of any type’s key paths with:
>
>
> let person = Person(firstName: “John”, lastName: “Doe”)
> let keyPaths = Person.keyPaths
> let firstNameKeyPath = keyPaths.first { $0.propertyName = “firstName” }
as! KeyPath<Person, String>
> let firstName = person[keypath: firstNameKeyPath] // “John"
>
>
> And finally, without straying too far into Objective-C land, it would be
nice if we could initialize key paths
> with a throwing initializer.
>
>
> let keyPath = try Person.keyPath(“firstName”) // KeyPath<Person, String>
type erased to AnyKeyPath
> let keyPath = AnyKeyPath(“Person.firstName”)
>
>
> Let me know what you think about any / all of these suggestions!

These would all be great additional features to eventually add to key
paths. I think reflection mechanisms centered on key paths like what you
describe would be a superior replacement for most of what Mirror attempts
to provide.

-Joe

OK. Swift already has protocol extensions, which let you provide default implementations alongside protocols.

-Joe

···

On Aug 25, 2017, at 1:45 PM, Eagle Offshore <eagleoffshore@me.com> wrote:

On Aug 25, 2017, at 1:35 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

What do you mean exactly by traits? That's an overloaded term.

http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf

Like PHP or Pharo traits. Like a protocol that has a default implementation that comes along with it.

How would you feel about wrapping the existing functions on _KVOKeyPathBridgeMachinery:

@nonobjc fileprivate static func _bridgeKeyPath(_ keyPath:AnyKeyPath) -> String
@nonobjc fileprivate static func _bridgeKeyPath(_ keyPath:String?) -> AnyKeyPath?

In extensions on String and AnyKeyPath respectively to instantiate strings from KeyPaths and KeyPaths from Strings?

Those functions are designed for Cocoa interop only. They're not going to produce results that make sense for all Swift key paths.

-Joe

···

On Aug 25, 2017, at 3:54 PM, Logan Shire <logan.shire@gmail.com> wrote:

https://github.com/apple/swift/blob/c5ff1f2cac8da6a14330f4b033b94c7c926d2126/stdlib/public/SDK/Foundation/NSObject.swift#L84

On Fri, Aug 25, 2017 at 11:43 AM Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

> On Aug 23, 2017, at 11:18 PM, Logan Shire via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>
> Hey folks!
>
> Recently I’ve been working on a small library which leverages the Swift 4 Codable protocol
> and KeyPaths to provide a Swift-y interface to CoreData. (It maps back and forth between
> native, immutable Swift structs and NSManagedObjects). In doing so I found a couple of
> frustrating limitations to the KeyPath API. Firstly, KeyPath does not provide the name of the
> property on the type it indexes. For example, if I have a struct:
>
>
> struct Person {
> let firstName: String
> let lastName: String
> }
>
> let keyPath = \Person.firstName
>
>
> But once I have a keyPath, I can’t actually figure out what property it accesses.
> So, I wind up having to make a wrapper:
>
>
> struct Attribute {
> let keyPath: AnyKeyPath
> let propertyName: String
> }
>
> let firstNameAttribute = Attribute(keyPath: \Person.firstName, propertyName: “firstName”)
>
>
> This forces me to write out the property name myself as a string which is very error prone.
> All I want is to be able to access:
>
>
> keyPath.propertyName // “firstName”
>
>
> It would also be nice if we provided the full path as a string as well:
>
>
> keyPath.fullPath // “Person.firstName"
>
>
> Also, if I want to get all of the attributes from a given Swift type, my options are to try to hack
> something together with Mirrors, or forcing the type to declare a function / computed property
> returning an array of all of its key path / property name pairings. I would really like to be able to
> retrieve a type-erased array of any type’s key paths with:
>
>
> let person = Person(firstName: “John”, lastName: “Doe”)
> let keyPaths = Person.keyPaths
> let firstNameKeyPath = keyPaths.first { $0.propertyName = “firstName” } as! KeyPath<Person, String>
> let firstName = person[keypath: firstNameKeyPath] // “John"
>
>
> And finally, without straying too far into Objective-C land, it would be nice if we could initialize key paths
> with a throwing initializer.
>
>
> let keyPath = try Person.keyPath(“firstName”) // KeyPath<Person, String> type erased to AnyKeyPath
> let keyPath = AnyKeyPath(“Person.firstName”)
>
>
> Let me know what you think about any / all of these suggestions!

These would all be great additional features to eventually add to key paths. I think reflection mechanisms centered on key paths like what you describe would be a superior replacement for most of what Mirror attempts to provide.

-Joe

I'm working on a library and want an ability to get the property name from a KeyPath. May I ask you what's the status of this pitch?

1 Like
Terms of Service

Privacy Policy

Cookie Policy