Refactor Metatypes, repurpose T.self and Mirror
01 何が問題だったのか
Swift にはもともと「型そのもの」を扱うための仕組みとして、メタタイプ(T.Type 記法と T.self 式)と Mirror 型が用意されていました。しかし両者が担う役割がはっきり分かれておらず、次のような複数の目的が混在していました。
- ジェネリック関数の明示的な特殊化(型を引数として渡すことで
Tを具体的な型に結び付ける) - 静的メソッドの動的ディスパッチ(メタタイプを通じて
staticメソッドやinitを呼び出す) - デバッグ用の値の木構造表現(
Mirrorの本来の用途) - 動的な型情報を取得するリフレクション(サイズやアラインメント、サブタイプ判定など)
このうち (1)(2)(4) がメタタイプ T.Type に、(3)(4) が Mirror に、それぞれ詰め込まれていたため、それぞれの型が「何のための道具なのか」が曖昧になっていました。
メタタイプの曖昧さ
メタタイプは型ではなくコンパイラ組み込みの特別な表現で、extension で拡張できません。そのため、静的メソッドディスパッチのために便利なメソッドを増やしたくても増やせないという制約があります。また、関数の特殊化に使う場合にはメタタイプのインスタンス自体の値には意味がなく、「型を運ぶための型」として過剰な表現になっていました。
ジェネリックコンテキストでの .Protocol 問題
より深刻なのは、プロトコルをメタタイプ経由でジェネリック関数に渡したときの挙動です。次のコードは直感に反して false を返します。
func intConforms<T>(to _: T.Type) -> Bool {
return Int.self is T.Type
}
intConforms(to: CustomStringConvertible.self) // => false
ジェネリックパラメータ T がプロトコル P のときの T.Type は、「P に適合する任意の型のメタタイプ」ではなく「プロトコル型 P 自身のメタタイプ(P.Protocol)」として振る舞います。このため、Int.self is T.Type は「Int のメタタイプが P.Protocol か」というまったく別の問いになり、適合判定として機能しません。
回避策として is T と書くと今度は .Type を付けるか付けないかで挙動が変わり、さらに Any.Type.self と Any.self の両方が通ってしまう、プロトコル同士の継承関係を判定する手段が事実上存在しない、といった細かな破綻が次々に現れます。sizeof / strideof / alignof といった標準ライブラリの関数が (_: Any.Type) ではなく <T>(_: T.Type) というジェネリック宣言を採っているのも、この .Protocol 問題を避けるためだと考えられていました。
Mirror の役割のずれ
一方の Mirror にも次のような問題がありました。
- 名前が「リフレクション」の印象を与えますが、本来はデバッグ用の木構造表現のための型です。
Mirror.DisplayStyleにoptionalやsetは特別扱いで含まれるのに、functionのような基本的なケースは含まれていません。- 初期化時にすべての情報を集めてしまうため、本格的なリフレクションに期待される遅延評価と相性が悪いです。
Array<T>が要素ごとに子として展開されるなど、デバッグ表示としては便利でもリフレクションの基盤には向かないカスタマイズが入っています。
結果として、デバッグ表示のための型が本格的なリフレクションの基盤としても期待されてしまい、どちらの目的にも最適ではない状態になっていました。
メタタイプと Mirror の役割を整理し、ジェネリックコンテキストでの .Protocol 問題を解消しなければ、この先にリフレクション機能を拡張していくための土台が整いません。そのための足場を作り直す必要がありました。
02 どのように解決されるのか
メタタイプが担っていた役割を、目的別に独立した型へと分解することを提案していました。具体的には Type<T>・Metatype<T>・Mirror の3つの型に役割を分け、従来の Mirror はデバッグ用途向けの別名に退避します。なお、本Proposalは最終的に withdrawn(撤回)となり、このままの形では採用されていません。
役割の分解
変更前と変更後で、それぞれの型が担う役割は次のように整理されます。
変更前:
T.Typeが「関数の特殊化」「静的メソッドの動的ディスパッチ」「動的キャストやsizeofなどによる部分的なリフレクション」の3役を兼ねる。Mirrorは主にデバッグ用途だが、限定的にリフレクションにも使われる。
変更後:
Type<T>は関数の特殊化専用。Metatype<T>は静的メソッドの動的ディスパッチ専用。Mirrorはリフレクションの基盤。DebugRepresentation(旧Mirror)がデバッグ用途を担う。
Type<T>: 関数の特殊化のための値型
T.self の意味を変更し、メタタイプではなく通常の構造体 Type<T> のインスタンスを返すようにします。Type<T> は普通の型なので extension で拡張でき、Hashable などのプロトコルにも適合できます。サイズは 0 バイトで、関数に型情報だけを渡すためのマーカーとして使います。
public struct Type<T>: Hashable, CustomStringConvertible, CustomDebugStringConvertible {
public init()
public static var size: Int { get }
public static var stride: Int { get }
public static var alignment: Int { get }
public static var metatype: Metatype<T> { get }
// インスタンス側にも同じプロパティが用意されます
}
使い方は従来の T.self とほぼ同じ感覚です。
func performWithType<T>(_ type: Type<T>) { /* ... */ }
performWithType(Float.self) // T.self が Type<T> を返すようになる
SE-0101 で MemoryLayout に集約された size / stride / alignment も、本Proposal版では Type<T> の静的プロパティとして提供される設計になっています。
Metatype<T>: 静的ディスパッチのためのメタタイプ
従来の T.Type 記法は Metatype<T> に改名します。メタタイプは引き続きコンパイラ組み込みの仕組みですが、公開 API の表記上は通常のジェネリック型と同じ形に揃うことになります。また、ジェネリックコンテキストで T がプロトコル P のとき Metatype<T> が P.Protocol に化けないよう、メタタイプの扱いを修正します。これにより、プロトコル適合の判定や、プロトコル同士の継承関係の判定が素直に書けるようになります。
Metatype<T> の本来の用途である静的メソッドの動的ディスパッチは、次のように記述します。
protocol HasStatic {
static func staticMethod() -> String
init()
}
struct A: HasStatic {
static func staticMethod() -> String { return "I am A" }
init() {}
}
struct B: HasStatic {
static func staticMethod() -> String { return "I am B" }
init() {}
}
func callStatic(_ metatype: Metatype<HasStatic>) {
print(metatype.staticMethod())
let instance = metatype.init()
print(instance)
}
let a = Type<A>.metatype
let b = Type<B>.metatype
callStatic(a) // => "I am A" / A()
callStatic(b) // => "I am B" / B()
公開 API から直接メタタイプを取り出したいときは Type<T>.metatype か T.self.metatype を使います。SE-0096 で導入された type(of:) は、戻り値がメタタイプであることを明確にするために metatype(of:) に改名します。
public func metatype<T>(of instance: T) -> Metatype<T>
Mirror: リフレクションの基盤へ
従来の Mirror(デバッグ用の木構造表現)は DebugRepresentation に改名し、対応するプロトコル CustomReflectable も CustomDebugRepresentable に、プロパティ customMirror も customDebugRepresentation に改名します。
その空いた Mirror という名前に、新しく「動的なメタタイプを持ち運び、型のサブタイプ関係や動的サイズを扱うための型」を導入します。Mirror は Metatype<Any> を 1 つ保持するだけの 8 バイトの値で、ジェネリックパラメータを取りません。これは、リフレクションの典型的な操作が「まず Any.Type に落としてから扱う」ためです。
public struct Mirror: Hashable, CustomStringConvertible, CustomDebugStringConvertible {
public init(_ metatype: Metatype<Any>)
public init<T>(_ type: Type<T>)
public init<T>(reflecting instance: T)
public var size: Int { get }
public var stride: Int { get }
public var alignment: Int { get }
public var metatype: Metatype<Any> { get }
public func `is`(_ mirror: Mirror) -> Bool
public func `is`<T>(_ type: Type<T>) -> Bool
public func `is`<T>(_ metatype: Metatype<T>) -> Bool
}
.Protocol 問題が解消されることで、Mirror の is(_:) メソッドがプロトコル型に対しても期待どおりに動作するようになります。これまで書けなかった「ある Any.Type が特定のプロトコルに適合しているか」の判定が、Mirror(reflecting: x).is(P.self) のように自然に書けるようになります。
既存コードへの影響
これはソース互換性を破る変更で、マイグレーションはおおむね機械的に行えるとされていました。代表的な対応は次のとおりです。
T.TypeはMetatype<T>へ書き換えます。T.selfは値の意味が変わるため、メタタイプとして使っていた箇所はType<T>.metatypeに置き換えます。- 旧
MirrorはDebugRepresentation、CustomReflectableはCustomDebugRepresentableに置き換えます。 sizeof(T.self)はType<T>.size、sizeof(metatype)はMirror(metatype).sizeに置き換えます。
変数の型を Metatype<T> から Type<T> や Mirror に持ち替えるためのパターン集も合わせて提示されていました。ただし、異なる型同士を行き来するキャスト(as? / as! / is のうち一部)は自動変換が難しく、人手での書き換えが必要になります。
今後の見通し(Future Directions)
この土台の上に、次のような発展が想定されていました(いずれも当時のアイデア段階の話で、確定的な予定ではありません)。
- SE-0090 が採用されれば、公開
.selfを廃止しTだけで型リテラルを書けるようにする。将来的には.dynamicType/.Type/.selfといった「マジックなメンバー」を言語から一掃できる可能性があります。 Mirrorにプロパティ一覧の取得のような本格的なリフレクション機能を追加し、Swift のリフレクション基盤として育てていく。
withdrawn の位置付け
本Proposalは、コミュニティおよびコアチームとの議論を経て withdrawn(撤回)となり、この形では Swift に取り込まれていません。Type<T> のような特殊化専用型の導入はその後の Swift でも採用されておらず、メタタイプは T.Type / T.self のまま残っています。ジェネリックコンテキストでの .Protocol 問題のように、本Proposalで指摘されたポイントのいくつかは後続の議論や実装改善で個別に扱われていくことになります。