DebugDescription Macro
01 何が問題だったのか
Swift では、値を人間が読みやすい文字列として表示するために CustomDebugStringConvertible プロトコルへの適合で debugDescription プロパティを定義できます。LLDB にも似た仕組みとして Type Summary があり、どちらも「プロパティをツリー状に展開したデフォルト表示では情報が多すぎるとき」に値を簡潔に表示するための機能です。
しかし、両者は定義を共有できません。debugDescription を定義しても、LLDB 側で恩恵を受けられるのは po コマンドで明示的に評価したときだけです。po は内部的に式評価(expression evaluation)を行いますが、式評価はコードを JIT コンパイルしてデバッグ対象のプロセスに流し込んで実行する重い処理で、次のような欠点があります。
- 任意のコードが走るため副作用があり得て、アプリケーションを不安定にしうる
- 遅い
- クラッシュログやコアファイルを扱う場面など、そもそも式評価ができない状況がある
こうした事情から、IDE の変数ビューのように常時表示される場所では式評価は行われず、リフレクションによる既定のツリー表示しか見えません。結果として、せっかく debugDescription を書いても、変数ビューでは各要素を展開しないと中身が区別できない、といった状況が起こります。
struct Organization: CustomDebugStringConvertible {
var id: String
var name: String
var manager: Person
var members: [Person]
// ... and more
var debugDescription: String {
"#\(id) \(name) (\(manager.name))"
}
}
この debugDescription はコンソールで po team と打ったときにしか使われず、変数ビューや配列の各要素のサマリ表示には反映されません。
LLDB 側の Type Summary は、Summary Strings と呼ばれる独自の(Turing 完全でない)文字列補間構文で書けます。プロパティへのアクセスはできますが、関数呼び出しや computed property のような計算は行えません。つまり、debugDescription のうち「stored property の値を文字列補間で並べているだけ」のシンプルな形は、機械的に Summary String へ変換できる可能性があります。
02 どのように解決されるのか
標準ライブラリに @DebugDescription マクロを追加します。これは型に付けるアタッチドマクロで、既存の debugDescription(または description / lldbDescription)を LLDB の Summary String に変換し、バイナリ側にメタデータとして埋め込みます。LLDB はそれを自動的に読み込むので、式評価を行わずにコンソール・変数ビュー・コレクション要素のサマリなど、あらゆる箇所で同じ表示が使われるようになります。
基本的な使い方
型に @DebugDescription を付けるだけです。
@DebugDescription
struct Organization: CustomDebugStringConvertible {
var id: String
var name: String
var manager: Person
var members: [Person]
// ... and more
var debugDescription: String {
"#\(id) \(name) (\(manager.name))"
}
}
この debugDescription はマクロによって次の Summary String に変換され、バイナリに埋め込まれます。
#${var.id} ${var.name} (${var.manage.name})
結果として、これまで po でしか得られなかった “#15 Shipping (Francis Carlson)” のような要約が、式評価なしで IDE の変数ビューや配列の各要素に対しても表示されるようになります。式評価を伴わないので速く、副作用がなく、クラッシュログやコアファイルを相手にするときにも使えます。
対応するプロパティ
@DebugDescription は次の順で対応プロパティを探します。
lldbDescription(独自プロパティ)debugDescription(CustomDebugStringConvertibleへの適合で定義するもの)description(CustomStringConvertibleへの適合で定義するもの)
lldbDescription は、「debugDescription / description を変えたくない/変えると String(reflecting:) や String(describing:) の挙動に影響してしまう」「Summary String 構文を直接使いたい」といった場合のための独自プロパティです。debugDescription と lldbDescription を両方定義して、デバッガには簡潔な lldbDescription を、print などには詳細な debugDescription を見せる、という使い分けも想定されています。
description に求められる条件
Summary String は計算ができないため、変換対象となる description 実装には次の条件があります。
- 本体は単一の文字列式であること。文字列の連結(
+)は不可で、文字列補間を使います - 文字列補間から参照できるのは stored property のみ。関数呼び出しや computed property は使えません。条件分岐も不可
- オーバーロードされた文字列補間は使えません
条件を満たさない description にマクロが付いた場合はコンパイル時に診断されるため、デバッガに届いてから動かないことに気づく、という事態は避けられます。
今後の見通し(Future Directions)
本提案の範囲外ですが、将来的な拡張の方向として次が挙げられています(現時点では実現を約束するものではなく、あくまで見通しです)。
- ジェネリック型への対応: 現状のマクロ実装は static property を型に追加する形を取っており、Swift のジェネリック型では使えません。「ジェネリックパラメータに依存しない具体型の static property はジェネリック型にも置ける」という言語側の拡張が別途必要です
- Summary String ではなく Python を生成する: 制約が緩くなる代わりにセキュリティ面での検討が必要になります
customMirrorの LLDB Synthetic Children への変換:ArrayやDictionaryのように、実装ではなくデータとしてのインタフェースを見せたい型で特に有用な方向性です