Pitch: Static and class subscripts

I like this idea and would love to use it in lieu of static factory methods (which I have a hard time naming):

enum Action {
    case rock, paper, bayonet
}
let reaction = ReactionFactory[.paper]

I think this feature will age well.

4 Likes

We do have PR for that: [CodeCompletion] Enable completion for subscript after dot by rintaro · Pull Request #20065 · apple/swift · GitHub . But AFAICT, providing [<#Index#>] completion after . (with erasing . instruction) was not so popular in internal discussion :sweat_smile: "not natural", "really confusing", "too magical", etc.

Another idea is to implement completion after expr[, just like after ( for methods. But this requires users to try typing [ without knowing the existence of subscript...

1 Like

For the record - I thought auto-complete provided this several times when I was hoping to learn what subscripts were available on a type.

In Xcode I would type [ waiting for a list to appear

5 Likes

These kinds of replacement completions are a popular and useful feature in other IDEs, for what it’s worth.

2 Likes

Did this rank subscripts below methods when you had typed a dot? If not, that might make a difference to users.

1 Like

Same here, i initially never had the need for this kind of feature, and the given examples didn't convince me at all, and looked rather confusing instead.

1 Like

I think this would be a great addition to the language. I've wanted to use this feature many times. As a mathematician, it warms my heart that it would likely be possible to write

struct R: Ring {
    static subscript<G: Group>(_ g: G.Type) -> GroupRing<R,G>.Type { ... }
}

and then have R[G] actually be the group ring type if G were a variable storing the metatype of some group. Clearly, this use case is rather silly, but it is fun!

3 Likes

Super +1, thank you for filling in this obvious missing feature!

2 Likes

Two questions. I think in Swift we want to allow explicit generic type on functions for disambiguation at some point. (e.g. function<Some>())

Do we plan to do the same for subscripts?

If so how will it look like on generic types that have static generic subscripts?

Type<T><R>[something]

If we wanted to do that, I think the syntax you mention is the most likely choice; an alternative would be Type<T>.<R>[something], which is less weird than it initially looks when you consider the \.[something] syntax for subscript key paths with inferred root types. But that's not something we're proposing today.

2 Likes

Adding my small voice to the general chorus of +1 for the core proposal.

I have one area of confusion, about the keypath syntax \Record.Type.tableName. After a bit of playground exploration I think I understand it, but I wonder if there are complications or surprises that aren't spelled out: perhaps this part of the proposal would benefit from some more explicit detail?

Things I think I understood or wonder about (I'm using Type here for the metatype throughout, assume no awkwardly-named members):

  1. \Record.Type.foo is a keypath rooted at Record's metatype and accessing Record's static member foo
  2. Is \Record.Type a valid keypath? Rooted at the metatype and accessing... what?
  3. (I assume \Record.Type.self is valid and is a keypath from Record's metatype to itself.)
  4. Is \Record.bar.Type.baz a valid keypath? (From what to what?)
  5. (Combining 2 and 4) \Record.bar.Type?
  6. Is there anywhere a .Type can appear in a valid keypath except as (the last) part of the root?

My tentative mental model is that the dot in \Record.Type is "special", and that it spells "Record's metatype as a keypath root": this would imply that 2, 4, and 5 above are all excluded. If this is correct, do we have any alternative notation that doesn't introduce this confusion? Or if it's incorrect, what's a better mental model?

Is the case perhaps analogous to nested declarations? In \Outer.Inner.foo I think of the first dot as part of the name Outer.Inner while only the second is for descending through structure; similarly the .Type notation would be "part of the name so-and-so's metatype" (which is only valid as a keypath root). Please do correct me if I'm barking wildly up the wrong mental model here.

It's not valid, but if you add .self it will be valid as this example is valid:

struct Outer {
  struct Inner {
    var value = ""
  }
}

let keyPath = \Outer.Inner.value

I don't think (2), (4), (5) or (6) are valid pathes.

You can fake these but that's about it.

struct Type {
  var baz: Int
}
struct Bar {
  var Type: Type
}
struct Record {
  var bar: Bar
}

\Record.bar.Type.baz

It is exactly this. Every type T has a nested type called T.Type; the static members of T are instance members of T.Type. We’re talking about forming a key path through that nested type, which is currently not permitted but should be.

3 Likes

Oh it's literally a nested-type name... that makes it all rather simple again, thanks!

1 Like

I don't really understand why this should be used in lieu of initializers.

One area where initializers fall short is when implementing a factory, where you want to be able to return various subclasses of a base class. It'd make more sense to me to implement factory initializers rather than using subscripts as a substitue. Especially since subscripts can't throw.

A subscript would be more justified for cases where you need a setter. But I haven't seen anyone express a motivation for set in a static subscript.

4 Likes

I could see it looking natural for something like TimeZone[identifier: ], but that doesn't read so much better than TimeZone(identifier: ) that I'd build the feature just for that.

1 Like

I’ve decided to defer key path support; the metadata work is more complex than I thought and there are some interesting design questions that deserve a separate proposal. (For instance, can you form a key path to an enum case? If you could, you could use an enum’s cases with SE-0252 to create an arbitrary, compile-time-checked set of dynamic members.) But this feature is still a necessary incremental step towards metatype key paths.

5 Likes

I'm strongly supportive of this feature since it eliminates an unnecessary special case from the language. My feedback is a handful of nitpicks:

I'm assuming you can also subscript a metatype value, like so:

let foo: Foo.Type = Bar.self
foo[...] = ...

I'm guessing the answer is yes, and it might be worth calling this out explicitly, with the added caveat that Foo[...] is a short-hand for Foo.self[...].

This feels like an unnecessary level of detail to me. You could just say that static and class can be applied to any subscript where they would otherwise be valid on a method or a property.

Also class extensions cannot define overridable members, so a class subscript in an extension is in fact equivalent to a static subscript. But again, its not worth going into this level of detail here.

However, you might want to point out that mutating getters and setters are not allowed on metatypes, since they're immutable. This is analogous to static properties.

8 Likes

That‘s an interesting find, I‘d be curious to know if we could form a keypath to enum cases with associated types and interpret the payload on the keypath as a tuple. However, since enum cases should be alligned with function I can see the argument against that, but then how would we ever access the payload? That feature would allow me to implement some partial UI updates more generically.

What do you have in mind here? Are you thinking of something along the lines of the key paths that would be possible if we had properties synthesized for the cases as in this pitch: Automatically derive properties for enum cases?