Pitch: Key-Path Member Lookup

I am suggesting, in the language of standardized college entrance exams: subscript(dynamicMember: String) is to #keyPath() as subscript(dynamicMember: WritableKeyPath) is to KeyPath, and that in light of that analogy, the existing @dynamicMemberLookup should where possible validate overloads just as #keyPath() does.

Expressions evaluable at compile time are being added to the compiler. These will compose nicely with @dynamicMemberLookup where implementations choose to use them. We may need specific additions to @dynamicMemberLookup to flesh this out but I, at least, expect that such a direction is inevitable.

1 Like

You did and it is. Thank you for providing a concrete example demonstrating the difference.

I guess technically this would work, but it’s a lot more subtle to communicate to a team that a specific use of a feature (i.e. a specific implementing signature) is banned than it is to communicate that an entire feature is banned (i.e. the attribute itself). I strongly prefer that we not muddy the water between statically checked and statically unchecked features.

Can you elaborate on what you have in mind here? This sounds quite beyond the capability of a feature that accepts arbitrary strings in the signature and allows code to do arbitrary things with them in the implementation.

Swift was designed to primarly interop with Objective-C? I feel like we are rehashing the original proposal. I think this new addition enhances the original proposal offering compile time checking when available.

An @inlinable implementation would be able to validate the arbitrary strings at compile time with sufficient constexpr-like features. (This is not the outer limit of what I would hope it can do: at the maximum possible level of optimization, if the implementation is interoperating with another LLVM-backed language, the whole dynamic member lookup where appropriate could be elided to a static lookup.)

In general, as I wrote in another thread, I expect the Swift compiler to perform at compile time any runtime check that it can, just as it optimizes many complicated expressions to performant primitives or even constants. The compiler already does a great deal of this sort of compile-time checking with integer operations, for instance. The surface syntax has in general preferred to gloss over runtime vs compile-time checking distinctions (see, for instance, SE-0213).

No we’re not. We’re discussing a new independent pitch. There has been some support for changing the pitch to merge it with the existing feature and I am pushing back on that direction.

Can you go a little bit deeper into how this would work? It isn’t clear to me yet.

Sure, consider the following toy example:

@dynamicMemberLookup
struct List<T> {
  var _value: [T] = []

  init(_ value: T...) { _value = value }

  @inlinable
  subscript(dynamicMember input: String) -> T {
    guard let x = Int(input), x < _value.count else {
      preconditionFailure("Index out of bounds")
    }
    return _value[x]
  }
}

let y = List(1, 2, 3)
// or, if we ever get `ExpressibleByTupleLiteral`:
// let y = (1, 2, 3)

y.0        // 1
y.1        // 2
y.2        // 3
y.3        // Fatal error: Index out of bounds
y.nonsense // Fatal error: Index out of bounds

Currently, accessing y.3 or y.nonsense is diagnosed as an error at runtime. However, given that 3 and nonsense are both constants, eventually the compiler will be able to diagnose the precondition failure at compile time.

1 Like

This is an interesting example, thanks for sharing it! I can see more clearly what you’re getting at now. I am interested in hearing the reaction of someone from the core team to this example. Is static reporting of a fatalError like this something that seems likely to be part of Swift in the future?

Sure; as another example, consider @scanon's thoughts about swizzling for SIMD types here:

Point being, it is not the case that @dynamicMemberLookup means no static checking. In fact, anticipate that there will be static checking where it makes sense, when it becomes possible to do so.

I assume @scanon had in mind that the string parsing and validation would be performed statically and and the member lookup would be transformed into a constant SIMD vector used as a subscript argument. Having the ability to do that kind of member validation and evaluation statically sounds very cool.

However, I do not think it makes sense to use a feature called @dynamicMemberLookup to do this. This is a way for code to participate statically in member lookup. Yes, a runtime implementation still remains, but it is somewhat analogous to the body of a computed property - a property whose lookup (index vector in this case) was statically resolved.

Sorry to come in so late with this, but I don't think we'd want to do this with expression matching anyway—just for example, introducing subtype conversions at this level would set a precedent where people use this feature to effectively create implicit conversions. That said, I do think signatures of the form subscript(dynamicMember: KeyPath<X, R>) -> R (where X and R may be generic parameters) should be treated specially when considering conformances, since they really are generalized forwarding functions semantically It would be reasonable to fall back to looking through these functions for potential matches and filling in witness tables with corresponding thunks.

3 Likes