StoredPropertyIterable

Regarding static key path schemas (StoredPropertyIterable), @Douglas_Gregor's reply above suggests defining APIs on MemoryLayout:

static var storedProperties can only provide key paths to static properties. However, note that instance-based key path schemas are necessary to support key paths to array elements and dictionary values:

  • Array.allKeyPaths: [WritableKeyPath<Self, Element>]: key paths to elements.
  • Dictionary.allKeyPaths: [WritableKeyPath<Self, Value>]: key paths to values.

We could exposed both static and instance-based key path schema APIs, which was the original intention of the pitch (StoredPropertyIterable vs CustomKeyPathIterable) and was suggested in some replies (like this one from @Joe_Groff). I'd like to focus on what an instance-based key path schema API could look like, since that's the more general API.


Regarding instance-based key path schemas, we could try something like:

/// A type that explicitly defines its own key path schema.
// Note: this is similar to `CustomReflectable`.
// Conforming types include `Array` and `Dictionary`.
protocol CustomKeyPathSchema {
  /// A collection of all custom key paths of this value.
  var allKeyPaths: [PartialKeyPath<Self>]
}

// Extending `Any` with `var allKeyPaths: [PartialKeyPath<Self>]` is not
// possible in Swift code. Instead, we can create an API that takes `Any`
// as an argument and provides `var allKeyPaths: [PartialKeyPath<Self>]`.

struct KeyPathSchema<T> {
  var value: T
  init(_ value: T) {
    self.value = value
  }

  var allKeyPaths: [PartialKeyPath<T>] {
    // Note: we need a `_CustomKeyPathSchema` implementation detail
    // similar to `_KeyPathIterable` to work around PAT limitations.
    if let customSchemaValue = value as _CustomKeyPathSchema {
      return customSchemaValue. _allKeyPathsTypeErased.compactMap { kp in
        kp as? PartialKeyPath<T>
      }
    }
    // Fallback: use runtime metadata to get all key paths to:
    // - Structs and classes: stored properties.
    // - Enums: associated values of the current enum case.
    // - Tuples: elements.
    ...
  }

  // Include existing `KeyPathIterable` default implementation utilities:
  // https://github.com/apple/swift/blob/tensorflow/stdlib/public/core/KeyPathIterable.swift

  /// An array of all custom key paths of this value and any custom key paths
  /// nested within each of what this value's key paths refers to.
  var recursivelyAllKeyPaths: [PartialKeyPath<T>] { ... }

  /// Returns an array of all custom key paths of this value, to the specified
  /// type.
  func allKeyPaths<T>(to _: T.Type) -> [KeyPath<Self, T>] {
    return allKeyPaths.compactMap { $0 as? KeyPath<Self, T> }
  }

  ...
}

// Usage:
struct Wrapper<T> {
  var item: T
  var array: [T]
}
let x = Wrapper<Float>(item: 0, array: [1, 2, 3])
for kp in KeyPathSchema(x).recursivelyAllWritableKeyPaths(to: Float.self) {
  x[keyPath: kp] += 1
}
print(x) // Wrapper<Float>(item: 1, array: [2, 3, 4])

Any thoughts?

I think some "key path view" wrapper abstraction is more natural than adding top-level functions like _forEachField to the global namespace.

2 Likes