Swift Digest
SE-0362 | Swift Evolution

Piecemeal adoption of upcoming language improvements

Proposal
SE-0362
Authors
Doug Gregor
Review Manager
Holly Borla
Status
Implemented (Swift 5.8)

01 何が問題だったのか

Swift では、既存のコードをコンパイルできなくしてしまうような変更(ソース互換性に影響する変更)は、原則として新しい言語バージョン(language version)をまたぐときにしか導入できません。Swift 5.x の段階で既にコンパイラには実装されている改善であっても、ソース互換性に影響するものは Swift 6 language mode がリリースされるまで利用できない、という状況が積み重なっていました。

この「Swift 6 を待たなければ使えない改善」の山が大きくなるほど、次のような問題が顕在化します。

  • 改善の恩恵を受けたい開発者は、Swift 6 のリリースまで長く待たなければなりません。
  • Swift 6 で一度にすべての変更を受け入れる形になるため、モジュールによっては移行コストが跳ね上がります。
  • 事前に試してもらってフィードバックを得る機会がなく、Swift 6 でいきなり本番導入することになります。

これに対してコミュニティは、個別の Proposal ごとに独自のオプトインフラグを増やすことで部分的に対処してきました。たとえば SE-0337 の -warn-concurrency や SE-0354 の -enable-bare-slash-regex がそれにあたり、SE-0335 の議論でも any を強制するフラグが欲しいという要望が挙がっていました。これらはいずれも「Swift 6 で入る予定の厳格化を、Swift 4.x / 5.x の段階で先取りしてオプトインしたい」という同じ形の要望ですが、Proposal ごとにフラグ名も運用も個別に設計されており、全体として統一感がありませんでした。

導入時期の異なる複数の改善を、一つひとつ段階的に(piecemeal に)取り込みつつ、Swift 6 への移行をなめらかにするための、言語レベルで統一された仕組みが必要でした。

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

「次の language version で有効になる予定の改善」を、モジュール単位で個別にオプトインできる統一的な仕組みとして upcoming feature flag を導入します。各 Proposal には UpperCamelCase 形式の feature identifier が割り当てられ、コンパイラフラグ・SwiftPM のビルド設定・ソース上の機能検出のすべてが、この同じ識別子を介して連動します。

コンパイラフラグ -enable-upcoming-feature

コンパイラに -enable-upcoming-feature X を渡すと、そのモジュールに対して feature X が有効になります。複数指定することもできます。

swiftc -enable-upcoming-feature ExistentialAny \
       -enable-upcoming-feature BareSlashRegexLiterals ...

この仕組みには、「将来の language version では標準で有効になる」改善だけが載ります。そのため、ある language version にアップデートして feature が既定で有効になると、同じ -enable-upcoming-feature X はもはや意味を持たず、コンパイラはエラーとして知らせます。古いマニフェストに残り続けて恒久的な方言を生むことを避けるための設計です。

逆に、コンパイラが知らない feature identifier が渡された場合は、エラーではなく無視されます。これにより、新しい feature を有効化したコードを古いコンパイラでもそのままのコマンドラインで扱えます。

本 Proposal と同時に、以下の既存 Proposal に feature identifier が割り当てられました。

  • ConciseMagicFile(SE-0274): #file の意味を #filePath から #fileID に切り替えます。
  • ForwardTrailingClosures(SE-0286): trailing closure の後方スキャンマッチングルールを取り除きます。
  • ExistentialAny(SE-0335): すべての existential 型に any を必須化します。
  • StrictConcurrency(SE-0337): 完全な並行性チェックを有効化します(-warn-concurrency 相当)。
  • ImplicitOpenExistentials(SE-0352): implicit opening の対象範囲を Swift 6 相当まで広げます。
  • BareSlashRegexLiterals(SE-0354): /.../ 形式の regex リテラルを有効化します。

SwiftPM からの指定

SwiftPM では、ターゲットごとの swiftSettings で upcoming feature を指定できるよう、SwiftSetting に API が追加されます。

extension SwiftSetting {
  public static func enableUpcomingFeature(
    _ name: String,
    _ condition: BuildSettingCondition? = nil
  ) -> SwiftSetting
}

使い方は次のようになります。

// Package.swift
.target(
  name: "MyLibrary",
  swiftSettings: [
    .enableUpcomingFeature("ExistentialAny"),
    .enableUpcomingFeature("BareSlashRegexLiterals"),
  ]
)

SwiftPM はこれらを、ビルド時に対応する -enable-upcoming-feature フラグへと展開してコンパイラに渡します。feature は文字列として渡すため、新しい feature が追加されるたびに Package.swift のマニフェスト形式を更新する必要はありません。

upcoming feature の効果はモジュール境界を越えないため、このターゲットに依存する別のターゲットが同じ設定を繰り返す必要はありません。

ソースコードでの機能検出 #if hasFeature(X)

#if の条件に hasFeature(X) を書けるようになり、feature X が有効なとき(-enable-upcoming-feature X か、それを既定で含む language version によって)に真となります。

#if hasFeature(ImplicitOpenExistentials)
  f(aCollectionOfInts)
#else
  f(AnyCollection<Int>(aCollectionOfInts))
#endif

hasFeature は、新しい構文を導入するような feature では、古いコンパイラがブランチの中身を解釈できずにエラーになる場合があります。そのようなケースでは、SE-0212 で導入された compiler(>=...) と組み合わせて、ネストさせて書くのが安全です。

#if compiler(>=5.7)
  #if hasFeature(BareSlashRegexLiterals)
  let regex = /.../
  #else
  let regex = #/.../#
  #endif
#else
let regex = try NSRegularExpression(pattern: "...")
#endif

あわせて #if の評価ルールも調整され、compiler(>=...)swift(>=...) を左辺に持つ && / || で結果が確定する場合、右辺の「関数呼び出しのような式」は構文として受理しつつも評価しないようになります。これにより、将来さらに hasAttribute(...) のような仕組みが増えても、古いコンパイラで #if 全体が拒否されずに済むようになります。

experimental feature との統一

同じ仕組みは、開発中の実験的な機能にも適用されます。-enable-experimental-feature X および SwiftPM の enableExperimentalFeature(_:) を使うことで、experimental feature を feature identifier 経由で有効化できます。experimental feature はリリース版コンパイラでは本来有効化されない不安定な機能ですが、のちに正式な言語機能へと昇格した際には、同じ identifier のまま hasFeature-enable-upcoming-feature に引き継げるため、段階的な導入のためのハブとして機能します。

「すべての upcoming feature を一括で有効化」は提供しない

本 Proposal では、全 upcoming feature を一度に有効にするようなフラグは意図的に提供しません。そうしたフラグがあると、Swift のリリースのたびに言語方言(dialect)が変動してしまい、既存コードが壊れうる状態になってしまうためです。どの feature が現在 upcoming として提供されているかは、swift.org 上でまとめて参照できるようにする方針です。