- Proposal: SE-0254
- Author: Becca Royal-Gordon
- Review Manager: Doug Gregor
- Status: Implemented (Swift 5.1)
- Implementation: apple/swift#23358
- Review: (review, acceptance)
We propose allowing static subscript
and, in classes, class subscript
declarations. These could be used through either TypeName[index]
or TypeName.self[index]
and would have all of the capabilities you would expect of a subscript. We also propose extending dynamic member lookup to static properties by using static subscripts.
Swift-evolution thread: Static Subscript (2016), Pitch: Static and class subscripts
Subscripts have a unique and useful combination of features. Like functions, they can take arguments to change their behavior and generic parameters to support many types; like properties, they are permitted as lvalues so their results can be set, modified, and passed as inout. This is a powerful feature set, which is why they are used for features like key paths and @dynamicMemberLookup
.
Unfortunately, unlike functions and properties, Swift only supports subscripts on regular types, not metatypes. This not only makes the language inconsistent, it also prevents us from supporting important language features on metatypes.
(Wait, what the heck is a "metatype"?)
A type like
Int
has many instances, like0
and-42
. But Swift also creates a special instance representing theInt
type itself, as opposed to any specificInt
belonging to that type. This special instance can be directly accessed by writingInt.self
; it is also returned bytype(of:)
and used in various other places. In fact, static members ofInt
are instance members ofInt.self
, so you use it any time you call one of those.Since
Int.self
is an instance, it must have a type, but the type ofInt.self
is notInt
; after all,Int.self
cannot do the things anInt
can do, like arithmetic and comparison. Instead,Int.self
is an instance of the typeInt.Type
. BecauseInt.Type
is the type of a type, it is called a "metatype".
And occasionally a subscript on a type is truly the best way to represent an operation. For instance, suppose you're offering access to the process's environment variables. Since the environment is global and environment variables can be both retrieved and set, a static subscript would be an excellent representation of them. Without them, users must either introduce a singleton instance or use static properties or subscripts to expose the same operations with less fidelity.
Swift originally omitted static subscripts for a good reason: They conflicted with an early sugar syntax for arrays, Element[]
. But we have long since changed that syntax to [Element]
and we aren't going back. There is no longer a technical obstacle to supporting them, and there never was a philosophical one. The only obstacle to this feature is inertia.
It's time we gave it a little push.
In any place where it was previously legal to declare a subscript
, it will now be legal to declare a static subscript
as well. In classes it will also be legal to declare a class subscript
.
public enum Environment {
public static subscript(_ name: String) -> String? {
get {
return getenv(name).map(String.init(cString:))
}
set {
guard let newValue = newValue else {
unsetenv(name)
return
}
setenv(name, newValue, 1)
}
}
}
The static and class subscripts on a type T
can be used on any expression of type T.Type
, including T.self[...]
and plain T[...]
.
Environment["PATH"] += ":/some/path"
A static subscript with the parameter label dynamicMember
can also be used to look up static properties on types marked with @dynamicMemberLookup
.
@dynamicMemberLookup
public enum Environment {
public static subscript(_ name: String) -> String? {
// as before
}
public static subscript(dynamicMember name: String) -> String? {
get { return self[name] }
set { self.name = newValue }
}
}
Environment.PATH += ":/some/path"
We do not currently propose to add support for metatype key paths, but this proposal is a necessary prerequisite for any future work on them.
One concern brought up during the pitch phase was discoverability. We think that code completion changes will help with this, but those are outside the scope of an Evolution proposal.
Static and class subscripts can be declared everywhere static and class computed properties can be, with analogous language rules. In particular, static and class subscript accessors are implicitly nonmutating
and cannot be made mutating
, just like static and class computed property accessors.
If a static or class subscript is declared on a type T
, it can be applied to any value of type T
, including T.self
, T
, and variables or other expressions evaluating to a value of type T.Type
.
Objective-C class methods with the same selectors as instance subscript methods (like +objectAtIndexedSubscript:
) will not be imported to Swift as class subscripts; Objective-C technically allows them but doesn't make them usable in practice, so this is no worse than the native experience. Likewise, it will be an error to mark a static or class subscript with @objc
.
@dynamicMemberLookup
can be applied to any type with an appropriate subscript(dynamicMember:)
or static subscript(dynamicMember:)
(or class subscript(dynamicMember:)
, of course). If subscript(dynamicMember:)
is present, it will be used to find instance members; if static subscript(dynamicMember:)
is present, it will be used to find static members. A type can provide both.
This proposal is purely additive; it does not change any previously existing behavior. All syntax it will add support for was previously illegal.
Static subscripts are an additive change to the ABI. They do not require any runtime support; the Swift 5.0 runtime should even demangle their names correctly. Dynamic member lookup is implemented in the type checker and has no backwards deployment concerns.
The rules for the resilience of static and class subscripts will be the same as the rules of their instance subscript equivalents. Dynamic member lookup does not impact resilience.
The main alternative is to defer this feature again, leaving this syntax unused and potentially available for some other purpose.
The most compelling suggestion we've seen is using Element[n]
as type sugar for fixed-size arrays, but we don't think we would want to do that anyway. If fixed-size arrays need a sugar at all, we would want one that looked like an extension of the existing Array
sugar, like [Element * n]
. We can't really think of any other possibilities, so we feel confident that we won't want the syntax back in a future version of Swift.
Swift does not currently allow you to form keypaths to or through static properties. This was no loss before static subscripts, since you wouldn't have been able to apply them to a metatype anyway. But now that we have static subscripts, metatype keypaths could be supported.
Metatype key paths were left out of this proposal because they are more complex than dynamic member lookup:
-
Making them backwards deploy requires certain compromises, such as always using opaque accessors. Are these worth the cost?
-
If we allow users to form key paths to properties in resilient libraries built before static key paths are supported, their
Equatable
andHashable
conformances may return incorrect results. Should we accept that as a minor issue, or set a minimum deployment target? -
Metatypes have kinds of members not seen in instances; should we allow you to form key paths to them? (Forming a key path to a no-argument case may be particularly useful.)
These issues both require more implementation effort and deserve more design attention than metatype key paths could get as part of this proposal, so it makes sense to defer them. Nevertheless, this proposal is an important step in the right direction.