SE-0252: Key Path Member Lookup

(Chéyo Jiménez) #21

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

(Adrian Zubarev) #22

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 } 
  }
}
[Pitch #2] Property Delegates by Custom Attributes
(Dan Zheng) #23

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

1 Like
(Dan Zheng) #24

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.

(Pavel Yaskevich) #25

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
(Adrian Zubarev) #26

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
(Dan Zheng) #27

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

1 Like
(Brent Royal-Gordon) #28

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
(Dan Zheng) #29

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:

(Adrian Zubarev) #30

@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.

(Adrian Zubarev) #31

@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
(Brent Royal-Gordon) #32

@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
(Pavel Yaskevich) #33

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.

(Adrian Zubarev) #34

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.

(Pavel Yaskevich) #35

@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
(Adrian Zubarev) #36

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
(Pavel Yaskevich) #37

Yes, we are working on improving this!

2 Likes