Static and class subscripts
01 何が問題だったのか
Swift の subscript は、関数のように引数を取りつつ、プロパティのように lvalue として使えて set / modify / inout も扱える、関数とプロパティの中間に位置する強力な宣言です。その性質を活かして、キーパスや @dynamicMemberLookup のような機能の土台にもなっています。
しかし、関数やプロパティが static / class 宣言を持てて「型そのもの(メタタイプ)」に対して呼び出せるのに対し、subscript だけはインスタンスにしか書けませんでした。言語として一貫性を欠くうえ、メタタイプに対してキーパスや @dynamicMemberLookup といった subscript ベースの機能を素直に提供する道もふさがっていました。
メタタイプと static メンバ
型 Int にはさまざまなインスタンス(0 や -42 など)がありますが、Swift は「Int 型そのものを表す特別なインスタンス」も用意しています。それを表すのが Int.self で、type(of:) の戻り値などもこれにあたります。Int.self は値ですが、その型は Int ではなく Int.Type です。この「型の型」をメタタイプと呼びます。
Int の static メンバは、実体としては Int.self(つまり Int.Type のインスタンス)のメンバです。static プロパティも static メソッドも呼べるのに、static subscript だけ書けないのは不自然でした。
型そのものに対する subscript が欲しい場面
たとえば、プロセスの環境変数を Swift から扱うことを考えます。環境変数はプロセス全体でグローバルに一つだけ存在し、取得も設定もできるため、「型そのもの」に対する subscript として表現するのが自然です。
// こう書けてほしい
Environment["PATH"] += ":/some/path"
しかし static subscript がなかったため、従来は次のような回避策が必要でした。
- シングルトンのインスタンスを用意し、そのインスタンス subscript を使う
- static な
get(_:)/set(_:forKey:)メソッドや、キーごとの static プロパティで代用する
いずれも、本来の「型そのものに対する添字アクセス」という意図を素直に表せず、+= のような複合代入や inout 経由での変更もやりづらくなります。
@dynamicMemberLookup の static 版もなかった
SE-0195 で導入された @dynamicMemberLookup は、subscript(dynamicMember:) を使ってインスタンスの「動的なメンバ」を解決する仕組みでしたが、static subscript がなかったため、型そのもののメンバ(Environment.PATH のような書き方)を動的に解決する手段はありませんでした。
歴史的な経緯
Swift には当初、配列型を Element[] と書く糖衣構文があり、この [] 記法と static subscript の文法が衝突するため、static subscript は見送られていました。その後、配列の糖衣は [Element] に一本化されて既に久しく、技術的な障害は解消済みで、残っているのは単に「後回しにしてきた」という慣性だけ、というのがこの Proposal の主張です。
02 どのように解決されるのか
これまで subscript が書けた場所ではどこでも static subscript を宣言できるようになり、クラスでは class subscript も宣言できるようになります。呼び出し側は、型 T に対する static / class subscript を T.self[...] でも、あるいは単に T[...] でも呼び出せます。
基本的な使い方
冒頭の環境変数の例は、次のように static 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)
}
}
}
Environment["PATH"] += ":/some/path"
get だけでなく set も持てるので、+= のような複合代入や inout への受け渡しも通常の subscript と同じように書けます。
宣言できる場所と言語ルール
static / class subscript は、static / class computed property が書ける場所ならどこでも宣言できます。ルールもそれらの computed property と同じで、アクセサは暗黙に nonmutating であり、mutating にはできません。
型 T に宣言された static / class subscript は、T.self や T、あるいは T.Type 型に評価される任意の式に対して適用できます。
なお、Objective-C との橋渡しについては、+objectAtIndexedSubscript: のような「インスタンス subscript と同じセレクタを持つクラスメソッド」は class subscript として Swift にインポートされません。また、static / class subscript に @objc を付けることもできません。
@dynamicMemberLookup の static 対応
@dynamicMemberLookup を付けた型は、インスタンスの動的メンバ参照を subscript(dynamicMember:) で実装しますが、これに加えて static subscript(dynamicMember:)(クラスでは class subscript(dynamicMember:))を実装すると、型そのものに対する動的メンバ参照にも対応できます。両方を同時に実装することも可能で、インスタンスメンバはインスタンス版、static メンバは static 版で解決されます。
これを使うと、先ほどの Environment は環境変数名をプロパティのように書いてアクセスできるようになります。
@dynamicMemberLookup
public enum Environment {
public static subscript(_ name: String) -> String? {
// 先ほどと同じ
get { return getenv(name).map(String.init(cString:)) }
set { /* 省略 */ }
}
public static subscript(dynamicMember name: String) -> String? {
get { return self[name] }
set { self[name] = newValue }
}
}
Environment.PATH += ":/some/path"
Future Directions: メタタイプのキーパス
Swift では現在、static プロパティを指すキーパスや、static プロパティを経由するキーパスを作ることはできません。static subscript が無かったこれまでは、仮にそういうキーパスがあってもメタタイプに適用する手段がなかったため大きな制約にはなりませんでしたが、static subscript が入ったことで、メタタイプに対するキーパスを将来サポートする道が開けます。
この Proposal 自体はメタタイプキーパスを含みませんが、resilience や後方互換性、case に対するキーパスをどう扱うかなど、別途検討が必要な論点が多いため、今後の検討課題として残されています。実現を約束するものではありませんが、その前提条件として static subscript が位置付けられています。