Swift Digest
SE-0356 | Swift Evolution

Swift Snippets

Proposal
SE-0356
Authors
Ashley Garland
Review Manager
Tom Doron
Status
Implemented (Swift 5.7)

01 何が問題だったのか

API や言語機能を読者に伝える際の「コード例」には、大きく分けて2つの形式があります。1つはビルド可能な完結したサンプルプロジェクト、もう1つはドキュメントの中に埋め込まれるコードリスティングです。しかし、どちらにもそれぞれの弱点があります。

サンプルプロジェクトはビルド構成・リソース・複数のソースファイルを含む本格的な「アプリ」として作られるため、作成にも維持にもコストがかかります。その結果、個々のトピックをコンパクトに示すというより、1つのプロジェクトにあらゆる機能を詰め込んだ「キッチンシンク」型になりがちで、読者が目当ての部分を見つけにくくなります。

一方、ドキュメントに埋め込まれるコードリスティングは、書き手がワードプロセッサ的な環境でそのまま入力することが多いため、次のような問題を抱えています。

  • 定期的にビルドされないので、言語や API の変更に追従できず、そのうちコンパイルできなくなっていく。
  • 書くときにエディタ / IDE の補完やエラー表示・シンタックスハイライトの助けを受けにくく、ミスが混入しやすい。
  • 実際にビルド・実行しないことが前提になるため、疑似コードに近くなり、読者がそのままコピーして動かせない。

ドキュメントの担当者が独自のシステムで抽出とテストを組むこともありますが、それ自体に大きな労力がかかり、本来のドキュメント執筆の時間を奪ってしまいます。

サンプルプロジェクトほど大げさではなく、しかしインラインのコードリスティングよりきちんと動作が保証される、「その中間」に位置する形式がないために、Swift の利用者は「小さくまとまっていて、コピー&ペーストしてすぐ使え、かつビルド・実行・テストまでできる」題材を作りにくいという状況がありました。

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

Swift パッケージに snippet という新しいサンプルコードの規約を導入します。snippet は単一の .swift ファイルとして書かれる実行可能な小さなプログラムで、パッケージの他のコードにもアクセスでき、ドキュメントへの埋め込み・コマンドラインからの実行・CI でのビルドなど複数の文脈で同じものを使い回せます。

snippet の書き方

snippet は、先頭のコメントで短い説明を与え、本体に示したいコードを書きます。説明は Markdown として扱われ、ドキュメントに埋め込まれたときに本文として表示されます。

// The first contiguous line comments
// serve as the snippet's short description.

func someCodeToShow() {
    print("Hello, world!")
}

// snippet.hide

func someCodeToHide() {
    print("Some demo message")
}

// Still hidden
someCodeToHide()

// snippet.show

someCodeToShow()

// snippet.hide// snippet.show は、ドキュメントに表示する範囲を切り替えるマーカーです。実行時にはすべてのコードが動きますが、ドキュメントに載るのは snippet.show 側のコードだけになります。このようにして、実行に必要なセットアップや検証コードを隠しつつ、提示用コード(presentation code)を整った形で見せられます。

スライス

ドキュメントでは、散文とコードを交互に並べて段階的に説明することがよくあります。その場合、前のコードブロックで定義した値を後のブロックから参照したい、ということが起こります。

// snippet.setup
let context = setup()

// snippet.request
context.request(.immediate)

// snippet.IDENTIFIER というマーカーで、1つのファイルの中を複数のスライスに分割できます。新しいスライスが始まると直前のスライスは自動的に終了し、隣接していない範囲を切り出したいときは // snippet.end を使います。snippet.hide / snippet.show と混在させることもできます。

snippet の配置

snippet を追加するには、パッケージルートに SourcesTests と並ぶ Snippets ディレクトリを作り、その中に .swift ファイルを置くだけです。ファイル名(拡張子を除いた部分)はパッケージ内で一意である必要があります。

MyPackage
├── Package.swift
├── Sources
├── Tests
└── Snippets
    ├── Snippet1.swift
    ├── Snippet2.swift
    └── Snippet3.swift

数が増えてきたときは、Snippets の直下にもう1段だけサブディレクトリを切って整理できます(それ以上深い階層は snippet としては扱われません)。

Snippets 以外の場所に置きたい場合は、Package イニシャライザに新しく追加される snippetsDirectory: 引数でディレクトリ名を指定できます。snippet ターゲットはマニフェストに個別に宣言するものではないため、設定はパッケージ単位です。

let package = Package(
    name: "MyPackage",
    snippetsDirectory: "Examples",
    products: [ /* ... */ ],
    dependencies: [ /* ... */ ],
    targets: [ /* ... */ ]
)

ビルドと実行

SwiftPM は Snippets 以下の .swift ファイルを自動的に検出し、それぞれを .snippet という新種のターゲット(実行可能ターゲットとほぼ同じ扱い)として構成します。snippet はホストパッケージのライブラリターゲットに自動的に依存するので、それらを自由に import できます。

テストと同様に、snippet のビルドは明示的に指示したときだけ行われます。

swift build                  # 通常のソースターゲットのみビルド(テストと snippet は除外)

swift build --build-snippets # snippet も含めてビルド

swift build Snippet1         # Snippet1.swift を実行可能としてビルド

swift run Snippet1           # Snippet1.swift を実行

CI 等では --build-snippets を付けてビルドすることで、snippet が古くなっていないかを継続的に検証できます。

snippet の「テスト」

snippet は「例」であって XCTest のようなテストではありませんが、期待する振る舞いを自分で確認したい場合があります。その用途には通常の assert / precondition を使います。precondition 等のチェックは実行時にそのまま失敗するので、CI で swift run するだけで例が壊れていないことを検出できます。検証用のコードは // snippet.hide の内側に入れておけば、ドキュメント上では本質的な部分だけが見える状態に保てます。

let numbers = [20, 19, 7, 12]
let numbersMap = numbers.map({ (number: Int) -> Int in
  return 3 * number
})

// snippet.hide
print(numbersMap)
precondition(numbersMap == [60, 57, 21, 36])

Swift-DocC からの参照

snippet はシンボルグラフ経由で DocC に伝えられ、DocC 側では新しいブロックディレクティブ @Snippet から参照できます。path は「パッケージ名 / Snippets / snippet 名」の3要素で、snippetsDirectory: を変更していても Snippets の部分は変わりません。

@Snippet(path: "my-package/Snippets/Snippet1")

スライスを指定したいときは slice: 引数に識別子を渡します。

@Snippet(path: "my-package/Snippets/Snippet1", slice: "setup")

@Snippet がスライスを指していない場合は、snippet の先頭コメント(Markdown として解釈)と presentation code の両方が展開されます。スライスを指している場合は、そのスライスに対応するコードブロックだけが埋め込まれます。

Future Directions

本 Proposal は「1ファイル1snippet」「ホストパッケージ内のライブラリに依存できる」という最小構成に絞っています。1ファイルに開始/終了マーカーで複数の snippet を書けるようにする拡張、snippet を外部パッケージに依存させる仕組み、既存のテストや大規模なサンプルプロジェクトから snippet 相当のコードをコンパイラレベルで抽出する仕組みなどは、今後の方向性として挙げられていますが、いずれも speculative で、実現を約束するものではありません。