SE-0252: Key Path Member Lookup

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

I have changed that error message to talk about both string and a keypath. I have also downloaded a of master toolchain from 10 Apr and tried by directly invoking swiftc from .xctoolchain directory, and code you have posted above type-checks just fine, so there might be something else going on in your project.

I pasted it into a simple iOS dummy project and it does not compile. I‘ll test tomorrow if a command line project will work or raise the same issue.

@DevAndArtist If it ends up returning old message it means that toolchain didn't get picked up by Xcode correctly, because that message no longer exists, it should diagnose as:

error: @dynamicMemberLookup attribute requires 'DriverFor' to have a 'subscript(dynamicMember:)' method that accepts either 'ExpressibleByStringLiteral' or a keypath
2 Likes

I can confirm that it worked this time. I probably needed only to restart Xcode yesterday (lesson learned).

I know that Xcode is not part of SE, but would it be possible to teach tools that provide autocompletion functionality to show the whole list of members we can now reach for each dynamic key-path member lookup subscript overload? That would be amazing and very helpful.

1 Like

Yes, we are working on improving this!

2 Likes

I'm wondering why automatic protocol conformance couldn't be provided in cases like the following. Clearly the Blogger struct conforms to the Note protocol.

protocol Note {
var title: String { get }
var url: URL { get }
}

struct Blog: Note {
let title: String
let url: URL
}

@dynamicMemberLookup
struct Blogger {
let name: String
let blog: Blog

subscript<T>(dynamicMember keyPath: KeyPath<Note, T>) -> T {
    return blog[keyPath: keyPath]
}

}