Extend implicit member syntax to cover chains of member references
01 何が問題だったのか
Swift には、文脈から型が推論できる場面で型名を省略して .zero や .systemBackground のように書ける implicit member expression(いわゆる先頭ドット構文)があります。
class C {
static let zero = C(0)
var x: Int
init(_ x: Int) {
self.x = x
}
}
func f(_ c: C) {
print(c.x)
}
f(.zero) // '0' と出力される
view.backgroundColor = .systemBackground
この構文は「型名だけを字面上省略したもの」と理解されがちですが、実際には 単一のメンバー参照 にしか使えませんでした。メンバー参照をチェーンすると、途端にコンパイルエラーになります。
extension C {
var incremented: C {
return C(self.x + 1)
}
}
f(.zero.incremented) // Error: Type of expression is ambiguous without more context
利用者から見ると、
let one: C = .zero.incremented
は
let one = C.zero.incremented
と同じ意味のはずですが、前者は通らないのです。この非対称性は特に「既存のインスタンスに変更を加えた新しいインスタンスを返す」ような modifier メソッドを持つ型で問題になります。たとえば UIColor.withAlphaComponent(_:) を先頭ドット構文と組み合わせることができません。
let milky: UIColor = .white.withAlphaComponent(0.5) // error
利用側としては自然に書きたい形が書けず、いちいち型名を書き足す必要がありました。
02 どのように解決されるのか
implicit member expression を、単一メンバーだけでなく メンバー参照のチェーン全体 に拡張します。文脈から型 T が推論できる場所で
.member1.member2.(...).memberN
と書いたとき、コンパイラはこれを
T.member1.member2.(...).memberN
と書いたのと同じように扱います。チェーン全体の結果型が T に一致するよう制約が張られるだけで、途中のメンバーの型は T と一致している必要はありません。
これにより、先ほどの例はそのまま通るようになります。
let milky: UIColor = .white.withAlphaComponent(0.5)
let milky2: UIColor = .init(named: "white")!.withAlphaComponent(0.5)
let milkyChance: UIColor? = .init(named: "white")?.withAlphaComponent(0.5)
なお T が Optional 型 R? のときは、チェーン先頭 member1 のルックアップを R? と R の両方で行う既存の規則がそのまま維持されます。上の milkyChance の例で .init(named:) が R 側のイニシャライザとして解決できるのはこの規則のおかげです。
チェーンで使える要素
チェーンのメンバーとして書けるのは次のものです。
- プロパティ参照
- メソッド呼び出し
- 強制アンラップ(
!) - オプショナルチェイニング(
?) - サブスクリプト
たとえば次のようなチェーンがすべて許されます。
struct Foo {
static var foo = Foo()
var anotherFoo: Foo { Foo() }
func getFoo() -> Foo { Foo() }
var optionalFoo: Foo? { Foo() }
var fooFunc: () -> Foo { { Foo() } }
var optionalFooFunc: () -> Foo? { { Foo() } }
var fooFuncOptional: (() -> Foo)? { { Foo() } }
subscript() -> Foo { Foo() }
}
let _: Foo = .foo.anotherFoo
let _: Foo = .foo.anotherFoo.anotherFoo.anotherFoo.anotherFoo
let _: Foo = .foo.getFoo()
let _: Foo = .foo.optionalFoo!.getFoo()
let _: Foo = .foo.fooFunc()
let _: Foo = .foo.optionalFooFunc()!
let _: Foo = .foo.fooFuncOptional!()
let _: Foo = .foo.optionalFoo!
let _: Foo = .foo[]
let _: Foo = .foo.anotherFoo[]
let _: Foo = .foo.fooFuncOptional!()[]
途中で型が変わってもよい
チェーンの途中では型が T と異なっていても構いません。最終結果さえ T に一致すればよいので、別の型を経由して戻ってくるような書き方も通ります。
struct Bar {
var anotherFoo = Foo()
}
extension Foo {
static var bar = Bar()
var anotherBar: Bar { Bar() }
}
let _: Foo = .bar.anotherFoo // Foo.bar: Bar -> Bar.anotherFoo: Foo
let _: Foo = .foo.anotherBar.anotherFoo
この「homogeneous でなくてよい」という設計は、キーパス式が \.foo.bar のように途中の型変化を許すのと揃っており、利用者の直感に沿います。
ジェネリック引数推論との組み合わせ
チェーンの各メンバーはジェネリック引数の推論にも参加できます。たとえば次のコードでは、すべての呼び出しで T が Int と推論されます。
struct Foo<T> {
static var foo: Foo<T> { Foo<T>() }
var anotherFoo: Foo<T> { Foo<T>() }
func getAnotherFoo() -> Foo<T> { Foo<T>() }
}
extension Foo where T == Int {
static var fooInt: Foo<Int> { Foo<Int>() }
var anotherFooInt: Foo<Int> { Foo<Int>() }
var anotherFooIntString: Foo<String> { Foo<String>() }
func getAnotherFooInt() -> Foo<Int> { Foo<Int>() }
}
extension Foo where T == String {
var anotherFooStringInt: Foo<Int> { Foo<Int>() }
}
func implicit<T>(_ arg: Foo<T>) {}
// いずれも T は Int と推論される
implicit(.fooInt)
implicit(.foo.anotherFooInt)
implicit(.foo.anotherFooInt.anotherFoo)
implicit(.foo.getAnotherFooInt())
implicit(.foo.anotherFooIntString.anotherFooStringInt)
型が合わないときの診断
チェーン全体の結果型が文脈から推論された型 T に変換可能でない場合、末尾 memberN の型 R を用いて次のような診断が出ます。
Error: Cannot convert value of type 'R' to expected type 'T'
具体的な表示は、T がどのように推論されたか(引数、明示的な型注釈など)によって変わります。