iOSエンジニアリング

宣言的 vs 命令的:SwiftUIで理解する

SwiftUIの紹介文書の冒頭には、必ずといってよいほど「宣言的(declarative)フレームワーク」という言葉が登場します。ReactもJetpack Composeも、自らを宣言的だと説明しています。

読了 4 分
宣言的 vs 命令的:SwiftUIで理解するのカバー画像

SwiftUIの紹介文書の冒頭には、必ずといってよいほど「宣言的(declarative)フレームワーク」という言葉が登場します。ReactもJetpack Composeも、自らを宣言的だと説明しています。

ところが「宣言的とは何ですか?」と聞かれると、答えに困ります。「コードがきれいということ」は正解ではありません。

宣言的と命令的の違いは、一文で分かれます。「How(どのように)」を書くのか、「What(何を)」を書くのかです。

この記事では、日常的な例でこの基準を捉え、UIKitとSwiftUIのコードで確認したうえで、宣言的が無料ではないことまで扱います。

要点をまとめます。

  1. 命令的:望む結果に到達する手順を段階的に指示する — How
  2. 宣言的:望む結果を記述し、手順はシステムに任せる — What
  3. SQL、HTML、map/filter、SwiftUIは宣言的アプローチの代表例
  4. 宣言的UIの本質:「状態がこうなら、画面はこうなる」と宣言すると、状態変化に応じた更新をフレームワークが行うこと

タクシーにたとえると

命令的とは、運転手に道順を直接伝えることです。「右折して、300メートル直進、信号で左折…」と手順を一つずつ指示し、その積み重ねで目的地に到着します。

宣言的とは「江南駅までお願いします」です。目的地(What)だけを伝え、経路の選択(How)は運転手とナビゲーションに任せます。

どちらも目的地に着きます。違いは、自分が手順を所有するのか、結果の記述だけを所有するのかです。


コードでは同じことを2通りの方法で書けます

偶数だけを選び、二乗するコードを2通りで書いてみましょう。

// 命令的:どう走査し、どこに格納するかを自分で指示する
var result: [Int] = []
for n in numbers {
    if n % 2 == 0 {
        result.append(n * n)
    }
}

// 宣言的:何が欲しいかだけを記述する
let result = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }

命令的なバージョンでは、ループ変数や中間配列、順序といった「手順の部品」が表に出ます。宣言的なバージョンに残るのは「偶数を絞り込み、二乗したもの」という意図だけです。走査方法はfilterとmapの内部に隠れています。

より身近な宣言的アプローチも、すでに使っているはずです。SQLでは「この条件の行をください」とだけ書き、インデックスの探索方法は書きません。HTMLも「ここに見出し、ここに段落」と構造だけを宣言し、描画手順は書きません。


UIでは違いが劇的に大きくなります

命令的UI(UIKit)は、画面を変更する手順を書きます。

// UIKit: 状態が変わるたびに画面操作を直接指示する
func updateBadge(count: Int) {
    if count > 0 {
        badgeLabel.isHidden = false
        badgeLabel.text = "\(count)"
    } else {
        badgeLabel.isHidden = true
    }
}

この方式の難しさは、「現在の画面がどの状態にあるか」を開発者が常に追跡しなければならないことです。更新経路が複数あると一つ抜けやすく、「データは変わったのに画面はそのまま」というバグになります。

宣言的UI(SwiftUI)は、状態に応じた画面の姿を宣言します。

// SwiftUI: count状態がこうなら画面はこうなる、と宣言する
struct BadgeView: View {
    let count: Int
    var body: some View {
        if count > 0 {
            Text("\(count)").badgeStyle()
        }
    }
}

countが変わったときに画面をどう直すかは書きません。SwiftUIが以前の宣言と新しい宣言を比較し、必要な部分だけを更新します。UIを「状態の関数」にしたのです。更新手順の所有権をフレームワークに渡したため、更新コードを入れ忘れる余地もありません。

命令的では自分が更新手順を書き、宣言的ではフレームワークが差分を計算する
命令的では自分が更新手順を書き、宣言的ではフレームワークが差分を計算する

宣言的は無料ではありません

バランスを取るために、反対側も見ておく必要があります。

手順を隠した代わりに、手順が必要なときはもどかしくなります。「スクロールを正確にこのオフセットへ移動して」のような命令的な要求には、宣言的フレームワークではむしろ回避策が必要です。

デバッグの質感も異なります。命令的なら手順を追えばよいのに対し、宣言的では「なぜ再描画されたのか」のように、フレームワークの判断を追跡する必要があります。隠れたHowを知らなくてよいわけではありません。

パフォーマンスチューニングも、結局は内部の理解に戻ってきます。SwiftUIでビューが過剰に再計算されるときは、解決のためにdiffingと依存関係の追跡がどう動くかを知る必要があります。

したがって正確な見方は「宣言的が優れている」ではなく、抽象化レベルの選択です。手順の所有権を渡せばコードは意図中心に単純になりますが、その代わり細かな制御と内部理解が課題として残ります。

手順の所有権を渡す代わりに、細かな制御を手放すトレードオフ
手順の所有権を渡す代わりに、細かな制御を手放すトレードオフ

まとめ

  • 命令的は結果に到達する手順(How)を指示し、宣言的は望む結果(What)を記述する
  • SQL・HTML・map/filterは、すでに身近な宣言的アプローチである
  • 命令的UIは画面を変更する手順を書き、状態と画面の不一致バグが起きやすい
  • 宣言的UIは「状態がこうなら画面はこうなる」と宣言し、更新はフレームワークに任せる(UI = 状態の関数)
  • 宣言的の代償は、細かな手順の制御が難しく、最終的にはフレームワーク内部の動作理解が必要になること
  • 本質は優劣ではなく、誰が手順を所有するかという選択である