SE-0252: Key Path Member Lookup

+1. This is a natural extension of the existing @dynamicMemberLookup. I’ve run into the need for this feature when writing lens projections very similar to those described in the proposal.

Yes.

Yes. It combines two orthogonal features — key paths and dynamic member lookup — in a way that is elegant and intuitive. Very Swifty.

Read the proposal, participated in the pitch thread.

+1

+1

I've been following the pitch and I'm really looking forward to this. It seems like a small feature, but it will greatly improve ergonomics of the types that will implement it.

+1

Yes.

Yes. In fact, I'd say Swift is less Swift-y without it.

N/A.

Quick read.

Almost all in favor, but there was one point that stuck out to me:

It's probably just me, but I feel like this statements assumes we can get the name of a property from the keypath, which you can't at the moment. Such a feature would come in very handy in case like this Fluent ORM change.

Yes.

Yes.

N/A

Just read the whole proposal.

Proposal Accepted

The review of this proposal has been unanimously positive. Leading up to the end of the review period the feedback tapered off, and the Core Team decided to move ahead and conclude the review a couple days early and accept the proposal.

The Core Team agrees with the discussion in the review that this proposal augments the existing dynamic member lookup facilities with additional static checking.

Thank you to everyone who participated in this review!

11 Likes

Could someone please rapid-fire some key-path member lookup use cases, other than Lens?
I personally couldn't think of any, actually.


From discussion with @rxwei and @blangmuir: static code completion for key-path member lookup types may be possible: at lens.<TAB>, make code completion look through the members of T in subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Lens<U>. Generalize this idea for all subscript(dynamicMember keyPath: ...).

An aside: on the other hand, code completion for string-based @dynamicMemberLookup is a bit dead in the water. Example: you have no idea how to code complete a function argument of type PythonObject (based on local knowledge - of which there is zero).

Wah.

I wonder if something like the below could be exposed to key paths for auto completion.

Take the example with storageValue from the property delegate proposal. I think the pitched idea of storageValue should be revisited as it‘s too magical and as @jrose pointed out in the other thread it shadows the property delegate type itself, which on the other hand makes it a perfect candidate for key-path lookup.

protocol Copyable: AnyObject {
  func copy() -> Self
}

@propertyDelegate
@dynamicMemberLookup
struct CopyOnWrite<Value: Copyable> {
  init(initialValue: Value) {
    value = initialValue
  }
  
  private(set) var value: Value
  
  var storageValue: Value {
    mutating get {
      if !isKnownUniquelyReferenced(&value) {
        value = value.copy()
      }
      return value
    }
    set {
      value = newValue
    }
  }

  subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
    get { return storageValue[keyPath: keyPath] }
    set { storageValue[keyPath: keyPath] = newValue } 
  }
}
class StorageManager {
  func allocate<T>(_: T.Type) -> UnsafeMutablePointer<T> { ... }
}

@propertyDelegate
@dynamicMemberLookup
struct LongTermStorage<Value> {
  let pointer: UnsafeMutablePointer<Value>

  init(manager: StorageManager, initialValue: Value) {
    pointer = manager.allocate(Value.self)
    pointer.initialize(to: initialValue)
  }

  var value: Value {
    get { return pointer.pointee }
    set { pointer.pointee = newValue }
  }

  var storageValue: UnsafeMutablePointer<Value> {
    return pointer
  }

  subscript<T>(
    dynamicMember keyPath: WritableKeyPath<UnsafeMutablePointer<Value>, T>
  ) -> T {
    get { return storageValue[keyPath: keyPath] }
    set { storageValue[keyPath: keyPath] = newValue } 
  }
}

Hmm. Is the idea to use key-path subscript(dynamicMember:) as a nicer interface than $storage?

1 Like

cc proposal authors: @Douglas_Gregor and @xedin
You must be busy - though if you have a second to illuminate us, that would be much appreciated.

From couple of things mentioned in the pitch ORMs could benefit from this. @Joe_Groff Also mentioned that "This could allow, for instance, a generic type to wrap a poorly-typed dictionary with its expected key-value mapping expressed as a tuple."

1 Like

The point is that you can use $storage already and it will be the delegate type. In the current pitch storageValue is something entirely new and it shadows the whole property delegate type. Assuming storageValue is of type Value, and that it has one member foo. $storage.storageValue.foo and $storage.storageValue would be valid Swift. However the presence of storageValue and its behavior makes the latter invalid as $storage itself is now seen as Value. The idea there is to allow $storage.foo without the need of an intermediate call to storageValue but that is too magical and exactly what key-path member lookup would already provide in a statically safe way.

1 Like

Thanks for the explanation!
I follow now - key-path member lookup enables the same natural COW API with less magic. :cow:

1 Like

The standard library could provide this:

@dynamicMemberLookup
public struct UnsafePointer<Pointee> {
  // ...as it is currently...
  public subscript<NewPointee>(dynamicMember: ReferenceWritableKeyPath<Pointee, NewPointee>) -> UnsafeMutablePointer<NewPointee>? {
    // standard library magic goes here--key path internals contain
    // enough data to make this work for stored properties in structs and classes.
  }
  public subscript <NewPointee>(dynamicMember: KeyPath<Pointee, NewPointee>) -> UnsafePointer<NewPointee>? {
    // The same, but immutable for immutable stored properties.
  }
}

So that you could say this:

let personPtr: UnsafePointer<Person> = ...
let postalCodePtr: UnsafePointer<PostalCode>? = personPtr.address.postalCode

(The types in that example could be inferred—I'm just writing them out to clarify the semantics.)

2 Likes

I see, that's neat.

To generalize things: if you have a type-safe way to access members, key-path member lookup allows you to sugar it nicely. This is a bit obvious in hindsight. :slightly_smiling_face:

@xedin just a quick clarification, sorry if I missed it somewhere. Will this feature with Swift 5 runtime? I just noticed I have a perfect use-case for it in my codebase, but our project will be locked to Swift 5 runtime feature set for quite some time now.


My use-case:

@dynamicMemberLookup
struct DriverFor<Base> {
  var base: Base
  
  init(base: Base) {
    self.base = base
  }
  
  subscript<T>(dynamicMember keyPath: KeyPath<Base, T>) -> T {
    return base[keyPath: keyPath]
  }

  subscript<T>(dynamicMember keyPath: WritableKeyPath<Base, T>) -> T {
    get { return base[keyPath: keyPath] }
    set { base[keyPath: keyPath] = newValue }
  }
}

It removes the need to call the intermediate base member almost completely.

@xedin I'm hitting this error with the development snapshot from April 10, 2019 with the example above:

@dynamicMemberLookup attribute requires 'DriverFor' to have a 'subscript(dynamicMember:)' method with an 'ExpressibleByStringLiteral' parameter

@DevAndArtist The proposal says:

This feature is implementable entirely in the type checker, as (effectively) a syntactic transformation on member access expressions. It, therefore, has no impact on the ABI.

That means it backwards deploys without issue.

5 Likes