Swift Digest
SE-0155 | Swift Evolution

Normalize Enum Case Representation

Proposal
SE-0155
Authors
Daniel Duan, Joe Groff
Review Manager
John McCall
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift 3 時点では、enum の case が持つ関連値(associated values)はタプルとして表現されていました。case elet(locals: [(String, Expr)], body: Expr) と書くと、裏側では ([(String, Expr)], Expr) というタプルが一つ付いている、という扱いです。この実装上の都合が、case の宣言・構築・パターンマッチのあちこちで一貫性のない挙動を生んでいました。

ラベルがコンストラクタ名の一部にならない

Swift の関数は SE-0111 以降、引数ラベルを含んだ完全名を呼び出し側で使えます。ところが enum の case コンストラクタではラベルが名前の一部として扱われないため、関数では書ける形が case では通りません。

enum Expr {
    indirect case elet(locals: [(String, Expr)], body: Expr)
}

func f(x: Int, y: Int) {}
f(x: y:)(0, 0)                         // OK: f(x: 0, y: 0) と等価
Expr.elet(locals: body:)([], someExpr) // Swift 3 ではエラー

デフォルト引数が書けない

関数なら引数にデフォルト値を書けますが、case のコンストラクタには書けませんでした。case fadeIn(duration: TimeInterval = 0.3) のような素直な宣言ができない、ということです。

パターンマッチの予期しない挙動

関連値がタプルであることは、パターンマッチ側にも漏れ出していました。

まず、値を一つだけ束縛するパターンが、関連値全体をタプルにまとめて受け取ってしまいます。

// 多くの人が期待しない形で「通ってしまう」
if case .elet(let wat) = anExpr {
    eval(wat.body) // wat は ([(String, Expr)], Expr)
}

また、宣言時のラベルがパターン側で強制されません。ラベルなしで書いたり、一部だけラベルを付けたりすることが許されてしまいます。

// 最初のサブパターンにはラベルがないのにマッチしてしまう
if case .elet(let p, let body: q) = anExpr {
    // ...
}

ペイロードなし case の紛らわしい書き方

さらに、関連値が一つもない case を空の括弧付きで書くことができていました。

enum Tree {
    case leaf() // Tree.leaf の型は (()) -> Tree という奇妙な形
}

これらは「case の関連値をタプルで表している」ことに起因する症状で、教えるにも拡張するにも余計なルールを抱え込む原因になっていました。

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

case の関連値を「タプル」としてではなく、enum case 専用の構文として扱うように整理します。宣言の見た目は Swift 3 とほぼ同じですが、ラベルは case コンストラクタの完全名の一部となり、デフォルト値も書けるようになります。パターンマッチもタプルパターンに相乗りするのをやめ、case 用のパターンとして独立します。

ラベルがコンストラクタの完全名に含まれる

関連値のラベルは case コンストラクタ名の一部になります。呼び出し時には、従来どおり引数列にラベルを書いても、関数と同じく完全名として書いてもかまいません。

Expr.elet(locals: [], body: anExpr)       // OK(Swift 3 と同じ書き方)
Expr.elet(locals: body:)([], anExpr)      // OK(完全名で参照)
Expr.elet(locals: body:)(locals: 0, body: 0) // エラー(二重にラベルを付けられない)

ラベルはタプルの一部ではなくなるので、型チェックにも関与しません。関数と同じ振る舞いです。

let f = Expr.elet // f の型は ([(String, Expr)], Expr) -> Expr
f([], anExpr)                   // OK
f(locals: [], body: anExpr)     // エラー

case は完全名で区別されるため、ベース名が同じで関連値のラベルだけが違う case を同居させることができます。

enum SyntaxTree {
    case type(variables: [TypeVariable])
    case type(instantiated: [Type])
}

// ベース名だけではあいまい
case .type                                  // エラー
case .type(variables: let variables)        // OK

関連値にデフォルト値を書ける

関連値の型の後ろに = 式 を付けると、その値にデフォルトを与えられます。

enum Animation {
    case fadeIn(duration: TimeInterval = 0.3)
}

let anim = Animation.fadeIn() // duration は 0.3

ペイロードなし case の書き方を厳密化

関連値を持たない case を空の括弧付きで書く case leaf() は認めなくなります。「関連値が Void 一つ」を表したい場合は、明示的にそう書く必要があります。

enum Tree {
    case leaf(Void) // 明示的に Void の関連値を持たせる
}

パターンマッチ側の取り扱い

Proposal には、パターン側もラベルの扱いを厳密化する案(ラベルを書くか、または宣言どおりのバインディング名を書くかの二択に揃える、など)が含まれていました。しかしこの部分はソース互換性を大きく損なうと判断され、最終的には「case のベース名が重複してあいまいになるときだけ完全名で曖昧性を解消する」という最小限のルールに落ち着きました。さらに Swift 5.2 時点でもそのルールは実装されず、対応する proposal は期限切れ扱いとなっています。結果として、パターンマッチのラベル取り扱いは Swift 3 からの振る舞いがおおむねそのまま残り、本 proposal のうち実際に言語に反映されたのは主に「コンストラクタの完全名化」「デフォルト引数」「空括弧 case の禁止」の三点です。