Build-Time Constant Values
01 何が問題だったのか
Swift の let プロパティや関数パラメータには、「値が実行時に確定する」ものが多く含まれます。たとえば、let は一度しか代入できないものの、初期化子の中では動的に計算した値を割り当てられますし、関数の引数も呼び出し側から任意の実行時値を渡せます。
しかし、API の設計者がパラメータやプロパティに対して「この値はコンパイル時点で決まっていてほしい」と要求したい場面は意外と多くあります。具体的には、次のようなユースケースが挙げられています。
- 属性やプロパティラッパの引数: たとえば
@Clamping(min: 0, max: 100)のように上限・下限を取るプロパティラッパで、min・maxに実行時値が入ってしまうと、同じ型のインスタンスごとに挙動が変わってしまい、コンパイラが最適化に活用することもできません。シリアライズ用のキー文字列なども同様で、実行時に計算した文字列がキーとして使われると、デシリアライズ時に破綻する原因になります。 - 非失敗型の初期化子:
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
struct や class の 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 値を読み取り専用メモリに配置して複数クライアント間で共有するといった実装を可能にするための制約です。
今後の展望(Future Directions)
本 Proposal は「宣言に対する @const 制約」という基本プリミティブだけを導入するもので、次のような発展が将来の Proposal に委ねられています。なお、これらはまだ設計段階の見通しで、実現を約束するものではありません。
- 推論と伝播: 現在は
@const let j = i(iが@const)のように@constな値どうしをつなぐことはできません。モジュール内部では自動的に@constと見なす一方、publicなものはSendableと同様に明示的オプトインのみとする、という方向性が検討されています。 - コンパイル時式・関数: 伝播の土台ができた上で、
#const_assert(input <= 0, "...")のようにコンパイル時に評価される式や、引数がコンパイル時既知なら自分もコンパイル時に評価できる関数を定義できるようにする構想が示されています。 - コンパイル時型: ユーザー定義型自体をコンパイル時既知の値として扱えるよう、カスタムリテラル構文や
@constイニシャライザを認める方向も議論されています。 - ツールチェーンによる値抽出: SwiftPM プラグインなどのビルド時ツールが、
@const値を汎用的に取り出せる仕組みの提供も展望として挙げられています。