SE-0479: Method and Initializer Key Paths

I am happy to report that we will be able to support argument labels for @dynamicMemberLookup using either compound names or fully applied methods, so the following will work with this feature:

struct Calculator {
  func multiply(this: Int, that: Int) -> Int { this * that }
  func multiply(this: Int, theOther: Int) -> Int { this * theOther}
}

@dynamicMemberLookup
struct DynamicKeyPathWrapper<Root> {
    var root: Root

    subscript<Member>(dynamicMember keyPath: KeyPath<Root, Member>) -> Member {
        root[keyPath: keyPath]
    }
}

let dynamicCalculator = DynamicKeyPathWrapper(root: Calculator())
let unapplied = dynamicCalculator.multiply // error: ambiguous use of 'multiply'
let partiallyApplied = dynamicCalculator.multiply(this: that:)
let fullyApplied = dynamicCalculator.multiply(this:4, theOther:6)
28 Likes

That's still true for both subscripts, and methods that need disambiguation, without parameters. But this previous post suggests that, because this will be possible:

struct S {
  func f(_: Int) -> Int { 0 }
}
\S.f(_:)

…which matches the currently available

S().f(_:) // (Int) -> Int

, subscripts with parameters ought to be referable by

\S.[_:]
extension S {
  subscript(parameter: Int) -> Int { 0 }
}

.

The trouble with that is that, unlike for methods, the following syntax is not available, for referring to subscripts as partially-applied closures.

S()[_:]

(The lack of argument label doesn't matter—it's just common.)


This is a good reference for the asymmetry I'm confused about:

struct S {
  func f(_: Int) -> some Any { () }
  func f() -> some Any { () }
  subscript(_: Int) -> some Any { () }
  subscript() -> some Any { () }
}

_ = S().f(1) // Compiles today.
_ = S().f(_:) // Compiles today.
_ = S().f() // Compiles today.
_ = S().f as () -> _  // Compiles today with explicit disambiguation.
_ = S()[1] // Compiles today.
_ = S()[_:] // Has never compiled. Is unaddressed.
_ = { S()[$0] } // Compiles today. How the above line must be spelled.
_ = S()[] // Compiles today.
_ = { S()[] } // Compiles today. There has never been proposed syntax to refer to this without this wrapping closure.

_ = \S.f(1) // Will be enabled by SE-0479.
_ = \S.f(_:) // Will be enabled by SE-0479.
_ = \S.f() // Unclear if this will be enabled by SE-0479.
_ = \S.f // Unclear if this will be enabled by SE-0479; would surely require explicit disambiguation.
_ = \S[1] // Compiles today.
_ = \S[_:] // Unclear if this will be enabled by SE-0479.
_ = \S[] // Compiles today.
// There has never been proposed syntax for referring to `[]` as a KeyPath<S, () -> _>.
3 Likes

That's great news! This is not reflected yet in the proposal text, but with this change, and with the future directions mention about effectful functions, I'm confidently +1 on the proposal, it feels complete on its own, it's open to future improvements, and it's going to make the language better and more complete.

1 Like

How would this work with functions with default values in argument fields?

struct Logger {
    func error(
        _ message: String,
        function: String = #function,
        file: String = #file,
        line: UInt = #line
    ) {

    }
}

@dynamicMemberLookup
struct DynamicKeyPathWrapper<Root> {
    var root: Root

    subscript<Member>(dynamicMember keyPath: KeyPath<Root, Member>) -> Member {
        root[keyPath: keyPath]
    }
}

Would following work?

let dynamicLogger = DynamicKeyPathWrapper(root: Logger())
let errorLogger = dynamicLogger.error
errorLogger("This is an error!")

or do you have to declare the full site?

let dynamicLogger = DynamicKeyPathWrapper(root: Logger())
let errorLogger = dynamicLogger.error(_:function:file:line:)
errorLogger("This is an error!")
3 Likes

I think it might be also nice to indicate that dynamicMember is accessing a method rather than a property, for example prefixing a call with $ like t.$doStuff(withThings: 3, etcetera: "hi"), it's probably a thing that could be addressed in a separate proposal, but smth like this might be a bit clearer for the caller

@dynamicMemberLookup
struct DynamicKeyPathWrapper<Root> {
  var root: Root

  subscript<Value>(
    dynamicMember keyPath: KeyPath<Root, Value>
  ) -> Value {
    root[keyPath: keyPath]
  }

  subscript<Value>(
    dynamicMember keyPath: WritableKeyPath<Root, Value>
  ) -> Value {
    get { root[keyPath: keyPath] }
    set { root[keyPath: keyPath] = newValue }
  }

  // projectedDynamicMember would be used to accept $-prefixed access
  // but basic dynamicMember label would also be acceptable
  // key difference is that a custom overload could accept a separate type
  // and method keypaths could be a subclass of KeyPath to
  // keep data a bit more stuctured as MethodKeyPath<Root, Input, Output>
  // for @dynamicMemberLookup Input type might be required to be Void 
  // since keyPath is the only argument we get here
  subscript<Value>(
    projectedDynamicMember keyPath: MethodKeyPath<Root, Void, Value>
  ) -> Value {
    root[keyPath: keyPath]()
  }

  subscript<Value>(
    projectedDynamicMember keyPath: MutatingMethodKeyPath<Root, Void, Value>
  ) -> Value {
    mutating get { // <- mutating methods can be called with `mutating get`s which are already supported in Swift
      root[keyPath: keyPath]()
    }
  }
}

Input could be a tuple with generated labels so that

class Calc {
  var subtract: (Int, Int) -> Int = { $0 - $1 }
  func subtract(a: Int, b: Int) -> Int { subtract(a, b) }
}

// Single-argument tuples should be allowed to support that
let subFrom10 = \Calc.sustract(a: 10) // MethodKeyPath<Calc, (b: Int), Int>

// partial application with synthesized callAsFunction method
let sub5From10: MethodKeyPath<Calc, Void, Int> = subFrom10(b: 10)

Calc()[keyPath: subFrom10] // (Int) -> Int
Calc()[keyPath: sub5From10] // () -> Int
Calc()[keyPath: subFrom10(b: 5)] // () -> Int

DynamicKeyPathWrapper(root: Calc()).$substract(a: 10, b: 10) // 0
DynamicKeyPathWrapper(root: Calc()).substract(10, 10) // 0

It's just some quick thoughts based on what I'd like to see for this repo that relies on dynamicMemberLookup heavily :sweat_smile:

In the current state my comment lacks some "thoughtthroughness", for example

  • "projectedDynamicMember" will conflict with projectedValue property wrappers, but I think it's a nice feature for dynamicMember lookup to be able to handle calls differently, just as one can do with propertyWrappers. Tho it should be addressed in a separate proposal I guess
  • I think the idea to have a different type and keeping labels in the type might be useful, but currently Swift doesn't support single-value tuples, I'm not sure if there is a specific reason for that

i realize the official review period is over, but i had a thought recently occur to me regarding this topic: since AnyKeyPath is Equatable, what are the semantics of comparing two method (or initializer) key paths?

2 Likes

Wanted to say thanks for your work on this! Really looking forward to what it will enable in the language.

Also was curious about any consideration for enum support. While I know enums are their own beast and will require their own key paths to fully encapsulate their behavior, I was wondering if the team has considered stopgap partial support considering this earlier pitch alongside this proposal's support for initializers.

For example:

enum Enum {
  case a
  case b(Int)
}

I could see \Enum.Type.a to produce a KeyPath<Enum.Type, Enum> and \Enum.Type.b to produce a KeyPath<Enum.Type, (Int) -> Enum>. And then, whenever the embedding functionality of enums is supported, these could become some form of CaseKeyPaths instead.

12 Likes

Another late-breaking thought: with class and KeyPath, you're guaranteed that the property is only read; you need ReferenceWritableKeyPath to modify the object. With method keypaths, this distinction is blurred, and I guess references to methods on classes must always return ReferenceWritableKeyPath (and should not be callable if upcast to KeyPath), to cover the ambiguity about whether the call might modify the object. Not 100% sure, but it feels like that calls into question the whole design?

1 Like