SE-0479: Method and Initializer Key Paths

Hello Swift community,

The review of "Method and Initializer Key Paths" begins now and runs through May 5, 2025. The proposal is available here:

swift-evolution/proposals/0479-method-and-initializer-keypaths.md at main · swiftlang/swift-evolution · GitHub

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email or Swift Forums DM. When contacting the review manager directly, please include "SE-0479" in the subject line.

Trying it out

If you'd like to try this proposal out, you can download a toolchain supporting it here. Any trunk development snapshot dated March 25, 2025 or later should do. You will need to enable the experimental feature KeyPathWithMethodMembers using SwiftSetting.enableExperimentalFeature in a Swift package manifest or the -enable-experimental-feature compiler flag in other build systems.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

swift-evolution/process.md at main · swiftlang/swift-evolution · GitHub

Happy reviewing,

—Becca Royal-Gordon
SE-0479 Review Manager

6 Likes

I think this is an important feature, and I'm looking forward to it. I'm a little scared by how light the proposal is; I don't feel like I know enough about KeyPaths to know whether there are new cases of actor isolation or sendability that need to be stated, or whether the existing KeyPath rules are sufficient.

I think the example about @dynamicMemberLookup should be clear that we've lost argument labels, and aren't gaining transparent method forwarding:

struct S {
    func doStuff(withThings: Int, etcetera: String) {
        // ...
    }
}

@dynamicMemberLookup
struct T {
    var s: S
    subscript(dynamicMember keyPath: KeyPath<S, T>) -> T {
        s[keyPath: keyPath]
    }
}

// ...

let t = T(s: S())
t.doStuff(withThings: 3, etcetera: "hi") // some kind of error about incorrect arg labels
t.doStuff(3, "hi") // accepted, disappointingly

I think the proposal should be clear about whether a mutating method requires a WritableKeyPath to be callable (I think yes?), whether a KeyPath to a consuming method can be formed (I think no), etc.

1 Like

Aww, I didn't realize we fully lost labels. Perhaps we can get them back when closure labels are added back (SE-0111).

The syntax for unapplied subscripts appears to be missing.

@dynamicMemberLookup

Discarding of argument labels makes this really not work with @dynamicMemberLookup as Swift APIs quite often differentiate just by argument labels, making those APIs ambiguous to call.
It would make it a source breaking change to introduce new method overloads that are just disambiguated by argument labels, essentially on any Type. It also would fail to disambiguate method and properties, making just this proposal source breaking even without further changes.
In fact, the @dynamicMemberLookup integration actually has the same problems as if we would allow KeyPath method references without argument labels as was discussed previously.

Even the example in the proposal shouldn't compile because subtract is ambiguous:

struct Calculator {
  var subtract: (Int, Int) -> Int { return { $0 + $1 } }
  func subtract(this: Int) -> Int { this + this}
  func subtract(that: Int) -> Int { that + that }
}

let dynamicCalculator = DynamicKeyPathWrapper(root: Calculator())
let subtract = dynamicCalculator.subtract // <- ambiguous, could be the property or one of the two methods.
print(subtract(10))

I think this either needs to support argument names (which I would really like but sounds rather difficult) or remove this integration.

Edit: I have tried it out with the latest nightly compiler.. This example seems to resolve to the property subtract. It still fails to compile because the call to subtract now expects two arguments because the subtract property returns a closure with two arguments. This should at least make this a non-source breaking for existing code.
However, as soon as users start using this new feature, it would still introduce ambiguities if a method with the same base name but different argument labels are introduced. That effectively would make it impossible to add new method overloads based on argument names to any type without risking to introduce source breaking changes for users that use @dynamicMemberLoop with method key paths. That is very undesirable.

mutating methods

I'm also quite concerned that mutating methods aren't supported, nor mentioned in the futures directions (there isn't even Future Directions section?). I can't really imagine how we could support mutating methods in the future without a very different strategy.

This also seems in direct conflict with the motivation section:

Key path methods and initializers will also enjoy all of the benefits offered by existing key path component kinds, e.g. simplify code by abstracting away details of how these properties/subscripts/methods are modified/accessed/invoked

Emphasis mine.

IMHO "value semantics" is one of greatest Swift feature. It would be quite unfortunate if we wouldn't support value types. As proposed this can only abstract modifications for types that have reference semantics.

consuming methods

It is a bit unclear how deeply consuming methods are actually supported. Does this actually support consuming methods without copying self or does this just convert it to a non consuming method and adds a copy before calling it? Concretely, would this trigger Copy on Write (CoW) or not?:

extension Array 
    consuming func appending(_ element: Element) -> Self {
        self.append(element)
        return self
    }
}
let array = [0, 1, 2]
// is CoW triggered here?
let newArray = array[keyPath: \.appending(3)]

I think it does need to do a copy as consuming get accessors aren't yet supported.
Is this something that could be added later? Any other way to not trigger CoW?


With that being said, I very much think this is a problem worth solving.
However, without a future direction nor any examples in the motivation section I don't really see a use case where I could use method key paths as proposed. It doesn't work for my use cases if it doesn't support mutating methods nor argument names with @dynamicMemberLoopkup.