Improved operator declarations
01 何が問題だったのか
Swift 2.2 までの演算子宣言は、宣言の書き方と優先順位の表現の両面で設計上の無理が生じていました。
宣言構文が場当たり的だった
既存の operator 宣言は、波括弧の中に precedence・associativity・assignment といったキーワードを並べる独自の形式でした。
infix operator <> { precedence 100 associativity left }
この構文は他の Swift の宣言とほとんど共通点がなく、文法的にも「単語を並べただけの袋」のような扱いで、Swift 全体のデザインから見て浮いた存在になっていました。
優先順位を数値で表すことの限界
中置演算子の優先順位は 0〜255 の整数で指定する方式でした。標準ライブラリの演算子は当初 90、100、110、……と 10 刻みのきれいな値に並んでいましたが、新しい演算子が追加されるたびにその隙間に数値を押し込む必要がありました。たとえば範囲演算子は 135、as は 132、?? は < より高く as より低いので 131、といった具合です。
数値方式には本質的な限界があります。二つの演算子の間にまた別の演算子を挟みたくなったときに、整数の隙間がなくなれば詰みます。<(優先順位 130)と ??(131)の間に新しい演算子を入れたくても、もう入る場所がありません。既存の数値を動かすことは ABI や既存コードを壊す破壊的変更になるため、事実上できません。
単一の優先順位ハイエラルキーが招く誤解
加えて、すべての中置演算子が同じ 1 本のハイエラルキー上に並ぶというモデル自体が、誤ったコードを許容する原因になっていました。ある演算子に優先順位を付けると、無関係な種類の演算子との関係まで一意に決まってしまうため、次のような式がコンパイラに何の警告もなく通ってしまいます。
a & b < c // (a & b) < c ではなく a & (b < c) と解釈される
a / b as Double // (a / b) as Double ではなく a / (b as Double) と解釈される
ビット演算と比較、算術とキャストのように、そもそも一緒に使うのが危うい演算子同士についても優先順位が定義されているせいで、C++ コンパイラが警告を出すような書き方でも Swift は黙って受け入れてしまっていました。根本原因は、「任意の 2 つの演算子の間に優先順位が定義されている」という前提そのものにあります。
02 どのように解決されるのか
演算子宣言の構文を刷新し、優先順位を数値ではなく precedence group(優先順位グループ)同士の半順序関係として表現する仕組みに置き換えます。これにより「整数の隙間が足りない」「無関係な演算子の間にも優先順位が定まってしまう」という二つの問題を同時に解消します。
新しい宣言構文
演算子宣言は本体を持たない、すっきりした形になります。中置演算子は所属する precedence group を : の後ろに書きます。
prefix operator !
infix operator +
infix operator <> : ComparisonPrecedence
precedence group は precedencegroup キーワードで宣言し、結合性(associativity)と他グループとの関係を記述します。
precedencegroup Additive {
associativity: left
}
precedencegroup Multiplicative {
associativity: left
higherThan: Additive
}
infix operator + : Additive
infix operator - : Additive
infix operator * : Multiplicative
higherThan は「このグループは指定されたグループより優先順位が高い」という関係を表します。1 + 2 * 3 は Multiplicative > Additive の関係から 1 + (2 * 3) と解釈されます。
関係が定義されていない演算子は曖昧
ここが従来との最大の違いです。二つの中置演算子の precedence group の間に関係が定義されていない場合、括弧なしで並べるとコンパイルエラーになります。
precedencegroup Additive { associativity: left }
precedencegroup BitwiseAnd { associativity: left }
infix operator + : Additive
infix operator & : BitwiseAnd
1 + 2 * 3 // OK: Multiplicative > Additive
1 + 2 & 3 // error: + と & の優先順位関係が定義されていない
& < < のような誤りやすい組み合わせは、標準ライブラリ側であえて関係を定義しないことで、書き手に括弧を強制できるようになりました。
関係は推移的に伝播する
higherThan 関係は推移律で自動的に拡張されます。たとえば Exponentiative > Multiplicative > Additive なら、Exponentiative > Additive も自動的に成立します。
precedencegroup Exponentiative {
associativity: left
higherThan: Multiplicative
}
infix operator ** : Exponentiative
1 + 2 ** 3 // 1 + (2 ** 3) と解釈される
関係全体は有向非巡回グラフ(DAG)として表現され、A > B > A のような循環を作る宣言はコンパイルエラーになります。
既存グループの下に挿入する lowerThan
自分で定義するグループを、別モジュールにある既存のグループより下に配置したい場面のために lowerThan が用意されています。higherThan と組み合わせて「上下から挟む」ような挿入ができます。
// 別モジュールで定義されたグループ Additive, Comparative があるとする
precedencegroup Equivalence {
higherThan: Comparative
lowerThan: Additive
}
infix operator ~ : Equivalence
1 + 2 ~ 3 // (1 + 2) ~ 3: Additive > Equivalence
1 < 2 ~ 3 // 1 < (2 ~ 3): Equivalence > Comparative
ただし、lowerThan で指定できるのは別モジュールのグループだけです。同一モジュール内では higherThan で十分であり、両方使えるようにすると同じ関係を複数の書き方で表せてしまうためです。
また、インポートされた無関係な二つのグループを、自分の新しいグループ経由でつなげてしまうような宣言は禁止されています。これは、サードパーティのコードが標準ライブラリのグループ間に予期しない関係を持ち込んで、読み手を混乱させるのを防ぐためです。
DefaultPrecedence と assignment
グループを指定せずに infix operator |> と書いた場合、その演算子は暗黙的に DefaultPrecedence グループに所属します。DefaultPrecedence は TernaryPrecedence より高い位置にあり、他の標準グループとの関係は意図的に定義されていません。したがってユーザー定義演算子をそのまま既存の算術・比較演算子と混ぜて書くことはできず、必要なら自分で precedence group を宣言するか既存グループを指定することになります。
従来の assignment 修飾子は、precedence group 側の assignment: true プロパティとして引き継がれます。これは foo?.bar += 2 を foo?(.bar += 2) のようにオプショナルチェインに畳み込むための挙動で、+= などの代入系演算子が所属する AssignmentPrecedence に指定されています。
標準ライブラリのグループ構成
標準ライブラリには、AssignmentPrecedence を最下位として、TernaryPrecedence → DefaultPrecedence / LogicalDisjunctionPrecedence → LogicalConjunctionPrecedence → ComparisonPrecedence → NilCoalescingPrecedence → CastingPrecedence → RangeFormationPrecedence → AdditionPrecedence → MultiplicationPrecedence → BitwiseShiftPrecedence という関係で並ぶグループ群が定義されます。+ は AdditionPrecedence、* は MultiplicationPrecedence、比較演算子は ComparisonPrecedence、&& は LogicalConjunctionPrecedence、といった具合に既存の演算子が各グループに振り分けられます。
注目すべきは、ビット演算子 &・|・^ とシフト <<・>>、そしてビット演算と算術演算・比較演算の間には関係が定義されていない組み合わせが残されている点です。a & b < c のような式は、この新しい仕組みのもとでは曖昧として拒否され、書き手に括弧を要求するようになります。
なお is・as・as?・as!・=・?: は Swift の構文として特別扱いされており、Swift コードで宣言できる普通の演算子ではありません。これらは CastingPrecedence・AssignmentPrecedence・TernaryPrecedence にコンパイラ側でハードコードされた形で所属します。
移行
標準ライブラリの演算子宣言は本提案の形式に書き直されます。ユーザー定義演算子については、マイグレーションツールが本体部分を削除し、infix 演算子は暗黙的に DefaultPrecedence に所属するようになります。演算子同士の優先順位関係に暗黙に依存していたコードは、このままではコンパイルできなくなるため、適切な precedence group を明示する修正が必要になります。本提案は Swift 3.0 で実装されました。