A misunderstanding of the final SE-0249 behavior

So I got a notice that SE-0249 finally got merged into master.

After the proposal was accepted there seem to have been some sort of alignment of the precise semantics.

Now in the past I discovered that key-paths have a requirement that every key-path literal parameter has to be hashable in order to form a valid key-path value. I opened up a thread and got an answer from Joe Groff that with SE-0249 closures that are formed using a key-path literal do not have to suffer from the same requirements as normal key-path values.

As for right now we cannot form key-paths over methods, but we eventually will be able to do so. Combined with SE-0249 I think it's fair to say that this hypothetical example is very tempting because it seems to be a very convenient way to get rid of some boilerplate code.

array.map(\.someFunction(someObject))

Now the newly added section in the proposal says the following:

The compiler may generate any code that has the same semantics as this example; it might not even use a key path at all.

That last part is important because it lets us assume that the key-path literal to form a closure might indeed no require parameter hashability.

However this seems to be not the case with the current implementation of the feature. Disclaimer: I do understand that one reason for that might be that the core seek for implicit transformation from key-paths into closures in some future.

Right now it's fairly rare that we use objects as subscript parameters, but this is a common thing with methods. And usually not every class conforms to Hashable protocol, which means that the above example won't compile with the current restrictions on the SE-0249 implementation.

The closest example we can write today with the toolchain from master might look like this:

class Object {}
let object = Object()

struct Value {
  subscript(_ object: Object) -> Object {
    object
  }
}

var array = [Value()]

// error: Subscript index of type 'Object' in a key path must be Hashable
let result: [Object] = array.map(\.[object])

This contradicts with the answer I received in the linked thread and with the quote from the proposal that explicitly says that the compiler might not use a key-path at all to form a closure from a key-path literal.

It means that the above closure formed using \.[object] literal probably decomposes into this code:

let closure: (Value) -> Object = { keyPath in 
  { value in 
    value[keyPath: keyPath] 
  } 
}(\Value.[object])

That of course won't compile because it uses a real key-path value which cannot drop the hashability requirement.

Based on the answer from the linked thread and the quote from the proposal however, I would have expected that the compiler won't use an actual key-path to form that particular closure. What I would have expected might look like this:

let closure: (Value) -> Object = { value in
  value[object]
}

Long story short, I invite everyone to discuss this issue in this thread and to clarify where we're heading right now.


P.S.: If my English failed somewhere, feel free to correct me. I'll be happy to edit. :wink:

I think that's a misinterpretation of the quoted section:

The compiler may generate any code that has the same semantics as this example; it might not even use a key path at all.

This is up to the compiler, not up to the user. Effectively, it's an optimization. There's room for expansion of this logic in the future (because it would always be backwards-compatible to lift this restriction), but it's hardly "an issue with SE-0249".

1 Like

I changed the title to be more precise what I personally want to discuss in this thread. Thank you for sharing your opinion on that.

1 Like

You might be correct here, but I thought that the composition of the closure should be predictable by the user, regardless of the possible optimizations the compiler might take.

That is definitely not the case for a number of Swift features. Consider inout: formally, this is a copy-in/copy-out operation on an argument, but in practice the compiler will often choose to modify the argument in-place. The implementation of SE-0249 is similar: formally, it must behave "as if" a key path is being used, but in practice it may not actually do a key-path-based access.

5 Likes

That is a fair point. However I was expecting that the final implementation won't be restricted to require parameter hashability, as I got high hopes from this response:

Is there any chance I could have misinterpret this one as well?

1 Like

I think that's the "expansion of this logic" point, and why your topic is definitely still valid! It just wasn't intended to be part of the initial feature. Or at least that's my reading of it.

2 Likes

I agree with Jordan. When I read this:

If it chooses (Root) -> Value , the compiler will generate a closure with semantics equivalent to capturing the key path and applying it to the Root argument.

My interpretation is that it's saying that "capturing the key path and applying it to the Root argument" would be a valid implementation of the proposal. That implies that it doesn't propose supporting anything that wouldn't be a valid key path. Since expanding the set of supported types would be a decision we would have to preserve source compatibility with down the road, I think we would need some action by the core team—either approving a new proposal or amending SE-0249—to expand the types it supports beyond what key paths allow.

(To be clear, I don't think it would be a controversial change if you had an implementation in hand, but I do think it would need approval.)

I don't think this says anything about the feature set of method key paths. It's not even clear if method key paths would involve the KeyPath types at all—they might return a closure, in which case Hashable conformance would probably be a moot point. I personally agree that method key paths, in whatever form they take, would be more useful if they did not require a Hashable conformance, but we can hash that out when we have a proposal for the feature. Maybe at that time we'll want to revisit the type constraints on the keypath-as-function feature, too.

3 Likes