Swift Digest
SE-0359 | Swift Evolution

ビルド時定数値

Build-Time Constant Values

Proposal
SE-0359
Authors
Artem Chikin, Ben Cohen, Xi Ge
Review Manager
Doug Gregor
Status
Returned for revision

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

Swift の let プロパティや関数パラメータには、「値が実行時に確定する」ものが多く含まれます。たとえば、let は一度しか代入できないものの、初期化子の中では動的に計算した値を割り当てられますし、関数の引数も呼び出し側から任意の実行時値を渡せます。

しかし、API の設計者がパラメータやプロパティに対して「この値はコンパイル時点で決まっていてほしい」と要求したい場面は意外と多くあります。具体的には、次のようなユースケースが挙げられています。

  • 属性やプロパティラッパの引数: たとえば @Clamping(min: 0, max: 100) のように上限・下限を取るプロパティラッパで、minmax に実行時値が入ってしまうと、同じ型のインスタンスごとに挙動が変わってしまい、コンパイラが最適化に活用することもできません。シリアライズ用のキー文字列なども同様で、実行時に計算した文字列がキーとして使われると、デシリアライズ時に破綻する原因になります。
  • 非失敗型の初期化子: Foundation.URL を文字列から作る際、その文字列がコンパイル時に確定していれば失敗しない初期化子を提供できます。StaticString は「ある定数文字列のどれかが使われる」ことしか保証せず、URL(Bool.random() ? "https://valid.url" : "invalid url") のように実行時に選ばれてしまう余地があります。
  • ビルド時のメタ情報抽出: Result Builder で記述する SwiftPM マニフェストのように、「パッケージ構造を表す値がすべてコンパイル時に分かる」ことを保証できれば、マニフェストを sandbox で実行せずにビルドツール側が情報を取り出せます。これは、宣言的な DSL でビルド時抽出可能な抽象を表現する基盤にもなります。
  • 最適化ヒントの保証: 数値演算の丸めモードのように、特定のパラメータだけを「必ずコンパイル時定数」にできると、コンパイラがより効率的なコードを生成しやすくなります。

これらに共通するのは、「この引数・プロパティだけは、リテラル的にコンパイル時確定であってほしい」という制約を、型ではなく宣言側で表明したいという要求です。Swift にはこうした要求を表現する手段がなく、ライブラリ作者はコメントや命名でしか意図を伝えられませんでした。

本 Proposal は、この土台として、コンパイル時に値が確定していることを要求する属性を導入します。なお、本 Proposal のステータスは Returned for revision で、最初のレビューの結果いったん差し戻されており、実装は main ブランチ上に _const という暫定名で存在しています。

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

プロパティ、ローカル変数、関数パラメータに付けられる @const 属性を導入します。@const が付いた宣言は、値がコンパイル時に確定していなければならず、実行時値を割り当てたり渡したりするとコンパイルエラーになります。名前マングリングへの影響はあるものの、実行時の挙動や型自体は変えません。

プロパティに付ける @const

structclass の stored property に @const を付けると、その値はコンパイル時に確定したリテラル相当でその場で初期化されなければなりません。通常の let と違い、イニシャライザ内で動的に代入することもできません。

struct Foo {
  // OK
  @const let superTitle: String = "Encyclopedia"
  // error: 'title' must be initialized with a const value
  @const let title: String
  // error: 'subTitle' must be initialized with a const value
  @const let subTitle: String = bar()
}

意味論としては「すべてのインスタンスで共有されるコンパイル時既知の値であることを、コンパイラに宣言する旗」というモデルです。現時点では @const let@const static let はコンパイラに与える情報としては同等に扱われます。

関数パラメータに付ける @const

関数パラメータに @const を付けると、呼び出し側で渡す引数がコンパイル時確定でなければなりません。

func foo(@const input: Int) { ... }

foo(11) // OK

let x: Int = computeRuntimeCount()
foo(x) // error: 'input' must be initialized with a const value

これにより、前述のプロパティラッパのキー指定や、丸めモードのようなパラメータに対して、ライブラリ側から「ここは必ずコンパイル時に決めてね」という契約を表明できます。

@propertyWrapper
struct SpecialSerializationSauce {
  init(@const key: String) { ... }
}

struct Foo {
  @SpecialSerializationSauce(key: "title")
  var someSpecialTitleProperty: String
}

プロトコル要件としての @const

プロトコル側で @const static let としてプロパティ要件を宣言すると、適合型側で「コンパイル時確定の値で初期化すること」を要求できます。通常のプロトコルプロパティ要件は var{ get } / { get set } を使いますが、値が実行時に変わらないことを前提とする @const では var を使うのは不自然なため、static let の形で書きます(getter のみが存在し、setter は無いことが @const から含意されます)。

protocol NeedsConstGreeting {
  @const static let greeting: String
}

struct Foo: NeedsConstGreeting {
  // OK
  static let greeting = "Hello, Foo"
}

struct Bar: NeedsConstGreeting {
  // error: 'greeting' must be initialized with a const value
  static let greeting = "\(Bool.random() ? "Hello" : "Goodbye"), Bar"
}

対応する型の範囲

@const を付けられるのは、今のところ次のいずれかに該当するリテラル値に限られます。

  • associated value を持たない enum のケース
  • 整数型・浮動小数点型((U)?Int(\d*), Float, Double, Half)、String(ただし文字列補間は不可)、Bool
  • 上記型のリテラルだけで構成される Array / Dictionary リテラル
  • 上記要素からなるタプルリテラル

将来的には、ここにより多くのリテラル種別やコンパイル時値の構成要素を追加していく方針が示されています。

public 宣言と ABI

public@const let については、値そのものがモジュールの ABI の一部として扱われます。ライブラリ側で public @const let x = 11 の値を変更することは ABI ブレークとなります。これは、将来 @const 値を読み取り専用メモリに配置して複数クライアント間で共有するといった実装を可能にするための制約です。

03 今後の見通し

本 Proposal は「宣言に対する @const 制約」という基本プリミティブだけを導入するもので、その先の発展は将来の Proposal に委ねられています。以下はあくまで将来の構想として示されているもので、実現を約束するものではありません。

@const 値の伝播と推論

現状の Proposal では、たとえば次のように @const な値を別の @const 宣言の初期化式や @const パラメータの実引数として使うことはできません。

func foo(@const i: Int) {
  @const let j = i // 将来的に許容したい
}

将来は、@const な値を別の @const 宣言や @const パラメータへ受け渡せるようにし、さらに @const 性をある程度自動で推論する方向が検討されています。推論については、モジュール内部の internal / private な値は自動的に @const と見なす一方、public なものは Sendable と同様に明示的にオプトインさせる、という方針が議論されています。

コンパイル時に評価される式と関数

伝播・推論の土台が整った先には、@const 値に対して評価できる式や関数を導入する構想があります。たとえば、#const_assert のようなコンパイル時アサーションで、@const 引数に対する条件を診断として記述できるようにする案が示されています。

func foo(@const input: Int) {
  #const_assert(input <= 0, "'foo()' expects a positive input")
}

これを成立させるためには、<= のような演算子も「引数がコンパイル時既知ならコンパイル時に評価できる関数」として宣言できる必要があり、どの関数がコンパイル時に評価可能かを推論する仕組みも別途設計することになります。

ユーザー定義型をコンパイル時値として扱う

現時点で @const を付けられるのは、リテラル相当の限られた標準ライブラリ型に限定されています。将来は、カスタムリテラル構文や @const 付きイニシャライザを通して、ユーザー定義型自体をコンパイル時既知の値として表現できるようにする方向も議論されています。これにより、コンパイル時値を扱う DSL の表現力を大きく広げることが期待されています。

ツールチェーンによる @const 値の抽出

@const は、SwiftPM プラグインのようなビルド時ツールから見ても有用な意味情報を運びます。将来的には、特定のクライアントに依存しない形で @const 値をビルド時に取り出すツールチェーン側のサポートを提供することで、Result Builder ベースのマニフェストのようなユースケースを横断的に支える展望が示されています。

@const 値の読み取り専用メモリ配置

@const 値の実行時のメモリ配置は、現時点では既存の値と変わりません。将来的には、グローバルな @const let を読み取り専用メモリに配置することで、初期化時の同期処理を不要にしたり、メモリ圧を下げたりする最適化を可能にしたい、という方向性が示されています。public@const let の値を ABI の一部として扱う本 Proposal の規定は、こうした将来の実装余地を残すための布石でもあります。