[swift-evolution] [Pitch] Improving KeyPath

Jon Gilbert swiftevolution at jongilbert.com
Wed Nov 29 15:52:30 CST 2017


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.) :D

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 at 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 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/20171129/9c92d56d/attachment.html>


More information about the swift-evolution mailing list