Swift Digest
SE-0299 | Swift Evolution

Extending Static Member Lookup in Generic Contexts

Proposal
SE-0299
Authors
Pavel Yaskevich, Sam Lazarus, Matt Ricketson
Review Manager
Joe Groff
Status
Implemented (Swift 5.5)

01 何が問題だったのか

Swift は、引数の型が文脈から分かっている場合、静的メンバーを先頭ドット構文(leading dot syntax)で短く書ける仕組みを持っています。たとえば SwiftUI には FontColor にあらかじめ定義された静的プロパティがあり、次のように書けます。

VStack {
    Text(item.title)
        .font(.headline)
        .foregroundColor(.primary)
    Text(item.subtitle)
        .font(.subheadline)
        .foregroundColor(.secondary)
}

.font(Font.headline) のように型名を書かなくても、font(_:)Font を受け取ることが分かっているため、型名の繰り返しを省いて読みやすさを保てます。

問題点

この便利な機能は、引数が 具体的な型 の場合にしか働きませんでした。引数がプロトコルを制約としたジェネリックパラメータの場合、先頭ドット構文は使えません。

SwiftUI の toggleStyle はこの典型例で、次のように ToggleStyle プロトコルに適合する任意の型を受け取ります。

public protocol ToggleStyle {
    associatedtype Body: View
    func makeBody(configuration: Configuration) -> Body
}

public struct DefaultToggleStyle: ToggleStyle { ... }
public struct SwitchToggleStyle: ToggleStyle { ... }
public struct CheckboxToggleStyle: ToggleStyle { ... }

extension View {
    public func toggleStyle<S: ToggleStyle>(_ style: S) -> some View
}

利用側は、具体的な適合型の名前を毎回書く必要がありました。

Toggle("Wi-Fi", isOn: $isWiFiEnabled)
    .toggleStyle(SwitchToggleStyle())

これには次のような不便さがあります。

  • toggleStyleToggleStyle を要求することは文脈から分かっているのに、SwitchToggleStyle のような長い名前を毎回書く必要があり冗長です。
  • 先頭ドット構文が使えないため、オートコンプリートで選択肢を列挙することもできず、どんな適合型が用意されているかを発見しづらくなります。

理想的には、具体型のときと同じように次のように書けることが望まれていました。

Toggle("Wi-Fi", isOn: $isWiFiEnabled)
    .toggleStyle(.switch)

既存の回避策とその問題

SwiftUI のベータ期には、StaticMember という汎用の箱型を挟むことでこの構文を模倣する API が存在しました。プロトコル側に typealias Member = StaticMember<Self> を置き、関数の引数型を S.Member にすることで、StaticMember の拡張として定義した静的プロパティを .switch のように書ける、という仕組みです。

しかし StaticMember は先頭ドット構文を成立させるためだけの型で、API の読み手から見ると役割が理解しづらく、SwiftUI も正式リリース前に取り除きました。フレームワークごとにこの種の回避策を持ち込むのではなく、言語レベルで一般的に解決したい、というのが出発点になりました。

02 どのように解決されるのか

プロトコルのメタタイプに対する静的メンバー参照の制限を 部分的に 緩和し、プロトコルのジェネリックパラメータに対しても先頭ドット構文を使えるようにします。ポイントは、プロトコルの拡張や静的メンバー自身で Self を具体型に束縛している場合に限り、その静的メンバーを先頭ドット構文で参照できるようにすることです。

基本ルール

静的メンバーを次のどちらかの形で宣言すると、先頭ドット構文で参照できるようになります。

  • Self を具体型に束縛する同一型制約(Self == 具体型)を持つプロトコル拡張で宣言されている
  • メンバー自身(ジェネリック関数・サブスクリプト)が Self を具体型に束縛する同一型制約を持つ

この条件を満たすメンバーを先頭ドット構文で書いたとき、コンパイラは暗黙の基底型をプロトコルそのものではなく Self が示す具体型に置き換えて、通常の静的メンバー参照として解決します。

ToggleStyle を例にした書き換え

前述の ToggleStyle は、新ルールのもとで次のように拡張できます。

public protocol ToggleStyle { ... }

public struct DefaultToggleStyle: ToggleStyle { ... }
public struct SwitchToggleStyle: ToggleStyle { ... }
public struct CheckboxToggleStyle: ToggleStyle { ... }

extension ToggleStyle where Self == DefaultToggleStyle {
    public static var `default`: Self { .init() }
}

extension ToggleStyle where Self == SwitchToggleStyle {
    public static var `switch`: Self { .init() }
}

extension ToggleStyle where Self == CheckboxToggleStyle {
    public static var checkbox: Self { .init() }
}

利用側は具体型の名前を書かずに済み、オートコンプリートで候補を眺めながら書けます。

Toggle("Wi-Fi", isOn: $isWiFiEnabled)
    .toggleStyle(.switch)

このとき、コンパイラは .switch を型検査済みの AST 上で SwitchToggleStyle.switch に置き換えています。基底型はジェネリック引数 S に課された ToggleStyle 適合の要件と、拡張の Self == SwitchToggleStyle 制約から一意に決まります。

ジェネリックな具体型への束縛

Self の束縛先は具体型であればよく、ジェネリックパラメータを持つ型でも問題ありません。メンバー自身が追加のジェネリックパラメータを取る形にもできます。

public struct CustomToggleStyle<T>: ToggleStyle { ... }

extension ToggleStyle {
    public static func custom<T>(_: T) -> Self where Self == CustomToggleStyle<T> {
        // ...
    }
}

Toggle("Wi-Fi", isOn: $isWiFiEnabled)
    .toggleStyle(.custom(42))
// 引数 42 から Self == CustomToggleStyle<Int> が推論される

この例では、呼び出し側の引数型から T == Int が決まり、それによって SelfCustomToggleStyle<Int> に束縛されて、.custom(42) が静的メンバー参照として成立します。

型検査の流れ

型検査器は、ジェネリック関数の呼び出し側から課されるプロトコル適合制約を推論し、その情報を先頭ドット構文の暗黙の基底型を表す型変数に伝搬させます。結果として基底型が具体型に絞れない場合は、推論されたプロトコル要件の型を基底として候補に据え、メンバー探索ではプロトコル拡張の静的メンバーまで対象を広げます。そのうえで、最も内側のジェネリックシグネチャにおいて Self が具体型に束縛されていることを確認し、その具体型で暗黙の基底を差し替えて参照を確定させます。

設計上の注意

この仕組みでは、SwitchToggleStyle.switch のような「具体型側の名前空間」にも switch 等のメンバーが生えることになります(Self == SwitchToggleStyle 制約の付いた拡張のメンバーは、その具体型の静的メンバーとしても見えます)。これは静的メンバールックアップを先頭ドット構文と整合させるためのトレードオフとして受け入れられています。

一方、戻り値の型を具体型にしただけで Self を束縛していない拡張は対象になりません。たとえば次のように書いた場合、

extension ToggleStyle {
    public static var `default`: DefaultToggleStyle { .init() }
    public static var `switch`: SwitchToggleStyle { .init() }
    public static var checkbox: CheckboxToggleStyle { .init() }
}

DefaultToggleStyle.checkboxSwitchToggleStyle.default のような、プロトコルに適合する任意の具体型からあらゆるメンバーが見えてしまう状態になり、名前空間が汚染されます。これを避けるため、本提案では Self を具体型に束縛する形の宣言だけを対象としています。

今後の見通し

本提案はあくまで段階的な緩和で、プロトコルのメタタイプそのものに拡張を書けるようにする一般的な仕組み(protocol metatype extensions)は別の課題として残されています。将来そちらが整備された際には、先頭ドット構文の解決方法も合わせて整理し直す余地が残されています。実現を約束するものではありません。