Swift Digest
SE-0287 | Swift Evolution

Extend implicit member syntax to cover chains of member references

Proposal
SE-0287
Authors
Frederick Kellison-Linn
Review Manager
Doug Gregor
Status
Implemented (Swift 5.4)

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 のように途中の型変化を許すのと揃っており、利用者の直感に沿います。

ジェネリック引数推論との組み合わせ

チェーンの各メンバーはジェネリック引数の推論にも参加できます。たとえば次のコードでは、すべての呼び出しで TInt と推論されます。

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 がどのように推論されたか(引数、明示的な型注釈など)によって変わります。