Enums and Dynamic Member Lookup

Currently dynamic member lookup is only possible with a subscript that has a string index. I think this should be extended to simple enums so that this is possible:

enum FontName { // maybe require `: String`
    case heading1, heading2, body1, body2
}

class FontBook {
    subscript(dynamicMember: FontName) -> UIFont { ... }
}

let fontBook = FontBook()
let font = fontBook.heading1
let font2 = fontBook.fooBar // Does not compile as fooBar is not a case in the enum

Currently this is kind of possible by doing:

enum FontName: String {
    case heading1, heading2, body1, body2
}

class FontBook {
    subscript(dynamicMember: String) -> UIFont { 
        guard let fontName = FontName(rawValue: dynamicMember) else {
            return someDefaultFont
        }
        ...
    }
}

let fontBook = FontBook()
let font = fontBook.heading1
// but this is also possible
let font2 = fontBook.fooBar

but this isn't ideal as there is no check at compile time. The alternative now is write a script that reads the enum case and generates methods for each case before the swift code is compiled but this isn't ideal.

2 Likes

Hmm, this is an interesting idea.

@dynamicMemberLookup provides a great syntactic sugar, but I feel its usefulness is limited because for most use cases, accepting arbitrary string indices (literally any nonempty string) is too general.

More safety is often desirable. I felt this strongly when writing tests for the @dynamicMemberLookup PythonObject struct. Any typo you might make (e.g. numpy.fill instead of numpy.full) will pass type-checking. Such typos were difficult to debug and left me scratching my head for a while. Thus, with the current design, it seems that informative error messages in subscript(dynamicMember:) (and dynamicallyCall) methods are essential for usability. This is kind of a smell to me: the usability of a language feature shouldn't rely so heavily on diagnostics.

A relevant recent example:

Without more safety, declaring @dynamicMemberLookup on SIMD vector types seems undesirable to me. There's much room for typos/errors and too little safety.

This pitch certainly addresses a real problem. I need to think more about it, but the idea seems promising because enum-typed indices can still be sugared in a simple-ish way, as you show in code. Interested in others' thoughts!

Then again, one could say: this is the fate you can expect when you sign up for dynamic language features. There's less safety for sure. But with @dynamicMemberLookup, I think there may be room for more safety; room for us to build more safety into the language feature itself.

3 Likes

This already works (except for a bug and potential improvement):

enum FontName: String, ExpressibleByStringLiteral {
// the `ExpressibleByStringLiteral` is the important part
  typealias StringLiteralType = String
  case heading1, heading2, body1, body2
  
  init(stringLiteral value: StringLiteralType) {
    self.init(rawValue: value)!
  }
}

@dynamicMemberLookup
class FontBook {
  // bug: internal name must be separately specified
  subscript(dynamicMember m: FontName) -> Int {
    get { return 1 }
  }
}

let fontBook = FontBook()
print(1)
let font = fontBook.heading1
print(2)
// improvement: constant fold the member name (& diagnose)
// currently crashes at runtime (in `FontName.init(stringLiteral:)`
let font2 = fontBook.fooBar
print(3)

It seems to me like you could define a method or subscript that takes a value of your enum type:

class FontBook {
  subscript(fontName: FontName) -> UIFont { ... }
}

let font = fontBook[.heading1]

Even with more checking, dynamic member lookup still obscures the underlying meaning of the code, since it's not really accessing a member, but passing a value derived from the member name to a special member, and the implicit members would not necessarily be discoverable via code completion, automatically-generated documentation or other standard tooling.

4 Likes

It seems both too magical and too limited to tie this to enums. I think this sort of compile-time validation should be based around the (previously pitched) compile time evaluation feature that will hopefully be added in future. This would allow much greater flexibility around how you validated the lookup, e.g. the SIMD “.xxyy” syntax that @dan-zheng mentioned would be better expressed by checking that the string only contains x, y, z, and w characters, instead of making an enum with all 256 possible combinations.

To hone on the vector swizzle use case: @dynamicMemberLookup plus compile-time evaluation is an interesting solution that avoids code bloat. Code completion isn't supported though, as stated by @Joe_Groff. I wonder if there may be other efficiencies or implementation challenges.

I feel the obvious approach is to use gyb to exhaustively define all .xxyy members. The only downside (not that it's an insignificant one) is massive code explosion: for a VectorN type of length N, there are N^N swizzles. I wonder what's the exact code size impact of such aggressive gybbing, and whether it's acceptable.

I don't think you would want them to appear in code completion anyway though, because it would both look ridiculous and not be very helpful. In my mind, there would perhaps be some more obvious swizzle method, a subscript taking an n-tuple of indices or whatever makes sense, and the shorthand member variables would just be an optional convenience where discoverability isn't that important.

Ah, good point. I agree that a single "swizzle method" endpoint is ideal for code completion.

Even with more checking, dynamic member lookup still obscures the underlying meaning of the code, since it's not really accessing a member, but passing a value derived from the member name to a special member, and the implicit members would not necessarily be discoverable via code completion, automatically-generated documentation or other standard tooling.

Yes we can use a standard subscript, and the syntactic sugar deliberately obscures the underlying meaning of the code, but that's the point. At the call site the user shouldn't need to know how it's implemented. Your point applies just as well to the original dynamic member lookup functionality.

It seems both too magical and too limited to tie this to enums. I think this sort of compile-time validation should be based around the (previously pitched) compile time evaluation feature that will hopefully be added in future. This would allow much greater flexibility around how you validated the lookup, e.g. the SIMD “.xxyy” syntax that @dan-zheng mentioned would be better expressed by checking that the string only contains x, y, z, and w characters, instead of making an enum with all 256 possible combinations.

I think that is a separate issue. In the the code I've given as an example the enum exists already. It's used elsewhere as a normal enum. What I'm suggested would just neaten the two the call site looks and abstract away from how the font is retrieved (ie maybe in a later version of the framework, dynamic member lookup isn't used and instead individual methods are generated or written).

This would just be another tool in the swift language that people could use if they wanted to help reduce the need for things like GYB.

// currently crashes at runtime (in FontName.init(stringLiteral:)
let font2 = fontBook.fooBar

I think if it crashes at runtime when an invalid name is specified then this is pretty much the same as just using the index as a string. But it's interesting that this is almost possible. it implies that this feature should be implemented as it 90% there with a bug.

I feel @jawbroken is trying to make the point (correct me if I'm wrong) that @compilerEvaluable plus current @dynamicMemberLookup functionality is a sufficient means for achieving the same end as enum-typed subscript(dynamicMember:) indices.

But the @compilerEvaluable solution is preferable because it involves less surface area: it doesn't involve extending @dynamicMemberLookup functionality, it only involves combining the existing @dynamicMemberLookup functionality with an orthogonal compile-time evaluation language feature.

OTOH, much of @compilerEvaluable isn't implemented yet (in particular, more work is needed to support compile-time string operations, needed here). :smiley:


@Joe_Groff's solution is also concise, type-safe, and everything we want (e.g. subscript(fontName: FontName) -> UIFont is a single endpoint for code completion).

2 Likes

Yeah, that's basically what I meant. In a future utopia where compile-time evaluation is pervasive, you could validate the call against an existing enum by simply trying to initialise it from the string raw value and checking if the failable initialiser returned nil or not. This gives you something like @Dante-Broggi's solution but with the force unwrap at runtime in the FontName initialiser being replaced by compile-time evaluation.

2 Likes

Quite the opposite, actually. The justification of the original dynamic member lookup functionality was precisely that it's needed to support the accessing of members; the argument was that member lookup should look like member lookup even in the interop setting, and that writing it as a subscript, for example, obscures the meaning of the code.

In your example, there's no dynamism required (in fact, the use case is precisely to constrain the options to a particular list known at compile time), and it's not a true member lookup. I would agree with @Joe_Groff that @dynamicMemberLookup isn't the right feature for this functionality.

1 Like

Also note that, in evaluating @dynamicMemberLookup, we extensively evaluated alternative solutions to dynamic language interop with the potential for better compiler integration, and only proceeded when we established that they wouldn't give as good results for the primary use case of importing libraries written in Python or other dynamic languages.

Sorry to deviate from the original topic, but what are your thoughts on vector swizzles?

There's no dynamism there either (all members are statically known), but the number of members is much larger, and there may be real negative consequences of defining all N^N members (e.g. code size explosion).

I wonder if that too might be best written as a subscript.

2 Likes

Aha. We can use the same approach from this thread and use a Swizzle enum whose cases are all the swizzles.

I strongly suspect an explosive number of enum cases is less detrimental than an explosion of swizzle computed properties (with getters and setters).

2 Likes

Why not a subscript that takes multiple arguments?

vector[.x, .x, .y, .y]
3 Likes