Swift & Objective-C

[Swiftの哲学 #2] Progressive Disclosureとは

Swiftには不思議な共存があります。Playgroundsでprint("Hello")の1行から始める初心者向けの言語でありながら、同じ言語でジェネリクスとマクロが絡み合う標準ライブラリが書かれています。小学生向けのコーディング教育アプリ(Swift…

読了 8 分
[Swiftの哲学 #2] Progressive Disclosureとはのカバー画像

Swiftには不思議な共存があります。初心者がPlaygroundsでprint("Hello")という1行から始める言語でありながら、同じ言語でジェネリクスとマクロが絡み合う標準ライブラリが書かれています。小学生向けのコーディング教育アプリ(Swift Playgrounds)とコンパイラレベルのシステムコードが、同じ構文を共有しているのです。

一般的な言語は、学びやすさ(Python系)か強力さ(C++系)のどちらかを選びます。Swiftは両方を備えると宣言し、それを可能にした設計原則には名前があります。段階的開示、Progressive Disclosureです。

この記事はSwiftの哲学シリーズ第2回です。第1回で扱ったSafe・Fast・Expressiveが「何を作るか」の哲学だとすれば、Progressive Disclosureは「どの順番で見せるか」の哲学です。

段階的開示とは — 必要になるまで隠す

Progressive DisclosureはもともとUI設計の用語です。よく使う機能だけを先に見せ、高度な機能は「もっと見る」の後ろに隠して、初心者を圧倒しない手法です。カメラアプリがシャッターボタンだけを大きく表示し、ISOやシャッタースピードをプロモードに隠すようなものです。

Swiftチームはこの原則を言語構文に適用しました。公式に掲げる目標でもあります。クリス・ラトナーは複数のインタビューでSwiftを「複雑さを段階的に開示する(progressive disclosure of complexity)言語」と説明しています。現在でもSwift Evolution提案のレビューでは、「この構文はprogressive disclosureを損なわないか」が審査基準になります。

原則を一文にまとめると、こうなります。

まだ学んでいない概念は、その人のコードに登場してはいけない。

単に「簡単な機能もある」という話ではありません。簡単なコードを書いている間は、難しい概念が視界にすら入ってはいけないという、はるかに強い要求です。

Hello Worldを比較する — 登場人物の数を数える

この原則が実際に何を変えるのかは、Hello Worldを比較するだけでも分かります。

// Java (11 前へ)
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

この5行を完全に理解するには、クラス、アクセス制御子、static、メソッド、配列、標準出力オブジェクトを知る必要があります。初日に学ぶコードに登場人物が6人いるのです。教師が言えるのは「まず暗記して、説明は後で」だけです。

// Swift
print("Hello, world!")

Swiftなら1行です。ファイルのトップレベルに実行コードをそのまま書け、セミコロンもimportもありません。登場人物は関数呼び出し1つ。「まず暗記して」は必要ありません。

核心はここです。Swiftにもクラス、アクセス制御子、staticはすべてあります。ただ、このコードには登場しないだけです。概念が存在しないのではなく、まだ必要のない人の目に見えないのです。

同じ機能の複数の層 — 構文が階段状に開く

Swiftの構文を分解すると、同じ機能が複数の難易度の層に重なっていることが分かります。代表例をいくつか見てみましょう。

**関数 → クロージャ → 省略クロージャ。**配列のソートなら、最初は名前付き関数を渡せば十分です。

func byLength(_ a: String, _ b: String) -> Bool {
    a.count < b.count
}
names.sorted(by: byLength)

クロージャを学べばインラインで書けます。

names.sorted(by: { a, b in a.count < b.count })

省略構文まで身につけると、こう短くできます。

names.sorted { $0.count < $1.count }

3つのコードは完全に同じ処理をします。トレーリングクロージャと$0を知らなくても初日からソートでき、学んだ後はより簡潔に書けます。新しい層を学ぶことは必須ではなく、報酬なのです。

型推論 → 明示的な型。let age = 30から始め、型注釈は必要になった瞬間(精度の指定やAPI境界)にだけ登場します。最初からlet age: Int = 30を強制しません。

**自動メンバーワイズinit → カスタムinit。**structは初期化コードを書かなくてもコンパイラが生成してくれます。初期化ロジックが必要になったときに初めてinit構文を学べばよいのです。

**エラーを無視 → try? → do-catch → 型付きthrows。**エラー処理にも、関心の度合いに応じて選べる階段があります。

よく使うものだけを見せ、高度な機能は後ろに置く、UIの段階的開示原則です。
よく使うものだけを見せ、高度な機能は後ろに置く、UIの段階的開示原則です。

視界の外にある複雑さ — 見えない場所で働くもの

構文の階段以上に印象的なのは、高度な機能が初心者向けコードを裏側から支えている構造です。

そのprint("Hello")printの実際の宣言は、このようになっています。

func print(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"
)

可変長引数、デフォルト引数、Any型。初心者がまだ知らない3つの概念が宣言に含まれていますが、呼び出し側は何も知らなくて構いません。デフォルト引数という高度な機能があるからこそ、初心者向けの使い方を単純にできます。高度な機能が複雑さを増やすのではなく、吸収する方向に使われているのです。

文字列補間("이름: \(name)")も同じ構造です。使う側にとっては最初の週に学ぶ基礎構文ですが、その下にはExpressibleByStringInterpolationプロトコルというカスタマイズ層全体が隠れています。この層のおかげで、SwiftUIのTextは文字列補間で画像や日付のフォーマットまで受け取れます。99%のユーザーは、このプロトコルの存在を知らないまま一生使いこなせます。その扉を開くのはライブラリ作者だけです。

SwiftUIはこの原則の集大成です。

struct ContentView: View {
    var body: some View {
        Text("Hello")
    }
}

この短いコードの下にはopaque type(some)、resultBuilder、プロトコルの関連型が敷かれています。いずれもSwiftで特に難しい機能ですが、UIを初めて作る人は存在すら知らずに画面を表示できます。「some Viewって何ですか?」という質問は、普通は数か月後に出てきます。そのとき学べばよいのです。

反対の例 — この原則がないと起きること

この原則の価値は、持たない言語と比べると明確になります。

C++を学ぶと、ポインタ、参照、コピーコンストラクタといったメモリの概念が初週から構文の表面に現れます。隠れているのではなく、避けられない関門です。Rustは安全性のため、所有権とライフタイムをすべてのユーザーに前払いで要求します。優れた設計ですが、「最初のプログラムを書くのに必要な概念の数」という基準では正反対の極端にあります。

興味深いのは、他の言語もSwiftと同じ方向へ移動してきたことです。Javaは21でvoid main()だけで実行できる暗黙的クラスを導入し(JEP 445、Java改善提案)、C#はトップレベルステートメントを追加しました。どちらも理由は同じです。初心者の最初のコードから儀式的な記述を取り除くこと。Swiftが2014年にデフォルトとして採用したものに、前世代の言語が10年かけて追いついたのです。

Text("Hello")の下にはresultBuilderとopaque typeが隠れています。
Text("Hello")の下にはresultBuilderとopaque typeが隠れています。

批判もある — 階段が崩れる地点

公平に言えば、Swiftがこの原則を完璧に守ってきたという評価だけではありません。

最大の批判は、言語が大きくなるにつれて中間の階段が急になったことです。入門は簡単ですが、実務コードやライブラリコードへ進んだ途端、ジェネリック制約、someとanyの区別、Sendableの記述が一度に押し寄せます。特にSwift 6のstrict concurrencyは、「並行処理をまだ学んでいない人のコードにコンパイルエラーを表示する」という点で、progressive disclosureと正面から衝突すると指摘されました。クリス・ラトナー自身も、Swiftが複雑になったという趣旨の発言をしています。

Swiftチームもこの緊張を認識しています。Swift 6.1以降の複数の提案(デフォルトのアクター隔離オプション、名前のないmainなど)が、「並行処理の概念に出会うまでは出会わないようにしよう」という方向で整理されていることが証拠です。この原則は完成した状態ではなく、今も守るために奮闘している価値に近いと見るのが正確です。

実務者への教訓 — API設計の基準として

この原則は言語のユーザーだけに関係する話ではありません。私たちが毎日作る関数やモジュールにも、そのまま適用できる設計基準です。

**デフォルト引数でよくあるケースを無料にする。**printのseparatorのように、90%の呼び出し側が気にしないオプションはデフォルトの後ろに隠します。設定オブジェクト全体を要求するAPIと比べてみてください。

**単純な入口を1つ用意し、高度なオーバーロードは後ろに置く。**URLSessionがよい例です。data(from: url)1行で始められ、デリゲートや設定が必要な人には別の層が開かれています。

**呼び出し側に見慣れない概念が漏れ出したら設計上のサイン。**自分のライブラリを使うためにジェネリックのシグネチャを読む必要があるなら、吸収すべき複雑さが漏れています。printが可変長引数を使いながら呼び出し側を単純に保つように、複雑さは宣言側が飲み込むべきです。

要するに、よいAPIとは機能が少ないAPIではなく、まだ学んでいない機能が見えないAPIです。

まとめ

  • Progressive Disclosureは、「まだ学んでいない概念をコードに登場させない」というSwiftの公式設計原則です。
  • 1行のprintによるHello World、型推論、自動init、クロージャ省略の階段は、すべてこの原則の産物です。
  • 高度な機能(デフォルト引数、resultBuilder、文字列補間プロトコル)は、初心者向けコードの複雑さを吸収する方向に設計されています。
  • Swift 6の並行処理のように原則が揺らいだ点もあり、言語チームは今も階段を修正しています。
  • 同じ基準を私たちのAPIにも適用できます。よくある利用は1行で、複雑さは宣言側に吸収させましょう。

次回は3つ目の哲学の話です。Swift標準ライブラリはなぜほとんどすべてstructで作られているのでしょうか。値型優先主義を扱います。

あわせて読みたい