Swift Digest
SE-0452 | Swift Evolution

整数ジェネリックパラメータ

Integer Generic Parameters

Proposal
SE-0452
Authors
Alejandro Alonso, Joe Groff
Review Manager
Ben Cohen
Status
Implemented (Swift 6.2)

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

01 何が問題だったのか

Swift のジェネリクスは長らく のみをパラメータとして受け取ってきました。つまり Array<Element> のように型を渡すことはできても、「要素数 4 の配列」のようにサイズ(整数値)を型の一部として表現する手段がありませんでした。

固定サイズコレクションが素直に書けない

固定サイズや固定容量のコレクション(インライン格納の配列、最大要素数が決まっている可変長バッファ、固定バケット数のハッシュテーブルなど)は、サイズ以外の実装はサイズに依存しません。本来であれば「サイズについてジェネリック」なライブラリ実装を 1 つ書き、利用側で Vector<4, Double> のように具体的なサイズを指定できるべきです。

しかし現状の Swift でこれを表現するには、特定の要素数を持つ struct をサイズごとに個別に定義したうえで withUnsafePointer などを駆使して添字アクセスを自前で実装する、といった強引な手段しかありません。結果として、4 要素の Vector と 5 要素の Vector を同じ汎用実装で賄うことができず、ライブラリ化が困難でした。

サイズを静的に保証する API が書けない

インラインストレージ以外にも、型情報として整数を持ち歩きたい場面は多くあります。たとえば、入力長や出力長を型に刻んでおけば、次のような API で「行列積の次元が連鎖的に一致していること」をコンパイル時に保証できます。

// こう書きたいが、サイズを型パラメータにできない
func matmul(_ l: Matrix<4, 2>, _ r: Matrix<2, 5>) -> Matrix<4, 5>

型パラメータに整数を載せられなければ、この種の静的保証は諦めるか、プロトコルや Collection 越しの抽象化に頼ってランタイムチェックに委ねるしかありません。

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

ジェネリックパラメータリストに 整数値のパラメータ(integer generic parameter)を書けるようにします。構文はジェネリックパラメータの山括弧の中に let <name>: Int を並べる形で、型パラメータと同じ位置に値パラメータが置けるようになります。

struct Vector<let count: Int, Element> {
    /* 実装は今回のスコープ外 */
}

このように宣言した型は、整数リテラルを引数にしてインスタンス化できます。

struct Matrix4x4 {
    var matrix: Vector<4, Vector<4, Double>>
}

周囲のジェネリック環境にある整数パラメータを、そのまま別の整数パラメータへ渡すこともできます。

struct Matrix<let columns: Int, let rows: Int> {
    var matrix: Vector<columns, Vector<rows, Double>>
}

型の static メンバとして公開される

整数ジェネリックパラメータは、その型と同じ可視性で static プロパティとして自動的に公開されます。別モジュールから Matrix<4, 3>.columns のように参照できます。

public struct Matrix<let columns: Int, let rows: Int> {
    // 暗黙的に以下のメンバを持つ
    //  public static var columns: Int { get }
    //  public static var rows: Int { get }
}

print(Matrix<4, 3>.columns) // 4
print(Matrix<4, 3>.rows)    // 3

同名の static プロパティをあとから宣言することはできず、名前の衝突はエラーになります。

関数やメソッドの型推論

関数・メソッドのジェネリックパラメータとしても使えます。通常のジェネリック引数と同様、呼び出し箇所の引数型から値が推論されます。行列積の次元の連鎖を型レベルで保証できるようになります。

func matmul<let a: Int, let b: Int, let c: Int>(
    _ l: Matrix<a, b>,
    _ r: Matrix<b, c>
) -> Matrix<a, c> { ... }

let m1 = Matrix<4, 2>(...)
let m2 = Matrix<2, 5>(...)

let m3 = matmul(m1, m2) // a = 4, b = 2, c = 5 → 戻り値は Matrix<4, 5>

式の中では Int の値として振る舞う

式の文脈で整数ジェネリックパラメータを参照すると、Int 型の値として評価されます。たとえば添字アクセスの境界チェックで count をそのまま使えます。

extension Vector {
    subscript(i: Int) -> Element {
        get {
            if i < 0 || i >= count {
                fatalError("index \(i) out of bounds [0, \(count))")
            }
            return element(i)
        }
    }
}

型と値の区別は保たれる

整数リテラルはジェネリック 引数 として書けるようになりますが、それ自体は型ではありません。整数を単独で型のように扱ったり、型パラメータの位置に整数を渡したりすることはできません。

let x: 2              // error: 2 は型ではない
let y: Array<2>       // error: Array の Element は型パラメータ

struct Foo<let x: Int> {
    let y: x          // error: x は型ではない
    let xs: Array<x>  // error: 型パラメータの位置に整数は渡せない
}

逆に、型パラメータや parameter pack を整数ジェネリックパラメータの位置に渡すのもエラーです。

同値制約は整数リテラル/別パラメータに対してのみ

整数ジェネリックパラメータには ==同値制約(same-value constraint)を書けます。整数リテラルに対する制約と、別の整数ジェネリックパラメータに対する制約の 2 通りです。

struct TwoIntParams<T, let n: Int, let m: Int> {}

extension TwoIntParams where n == 2 {
    func foo() { ... }
}

extension TwoIntParams where n == m {
    func bar() { ... }
}

let x: TwoIntParams<Int, 2, 42>
x.foo() // OK
x.bar() // Error

let y: TwoIntParams<Int, 3, 3>
y.foo() // Error
y.bar() // OK

型パラメータや具体型(Int そのもの)、グローバル定数との同値制約は書けません。また、プロトコルへの conformance 制約(n: Collection のような形)も整数ジェネリックパラメータには付けられません。

制約付き extension の解決は、通常のオーバーロード解決と同じく 呼び出し側の型情報が制約を満たしているときだけ そのメンバが選ばれます。制約の緩い文脈からは、制約が偶然満たされていても具体化された実装にはディスパッチされません。

struct Foo<let n: Int> {
    func foo() { print("foo #1") }
    func bar() { self.foo() } // ここからは常に foo #1 が呼ばれる
}

extension Foo where n == 2 {
    func foo() { print("foo #2") }
}

Foo<2>().bar() // foo #1
Foo<2>().foo() // foo #2

型は Swift.Int に限る

値パラメータとして書ける型は、標準ライブラリの Swift.Int に解決される型だけです。typealiasassociatedtype を経由して Swift.Int に行き着く場合は OK ですが、ローカルに定義された別の Int 型や Float などは使えません(他の型への拡張は将来の検討課題です)。

struct Foo<let x: Int> {}        // OK
struct Foo2<let x: Swift.Int> {} // OK
struct BadFoo<let x: Float> {}   // Error

命名規約

整数ジェネリックパラメータは「値」の一種なので、プロパティや関数と同じく lowerCamelCase で名付けることが推奨されます(型パラメータの UpperCamelCase とは区別します)。この規約は、将来的に定数や式を整数ジェネリック引数として渡せるようになったときにも、記述のスタイルが一貫するよう選ばれています。

Back-deployment の制約

整数ジェネリックパラメータを一般に扱うためには、型メタデータ上で整数値を表現・解釈する Swift ランタイムの新機能が必要です。OS が Swift ランタイムを同梱するプラットフォーム(Apple 系)では、古い OS バージョンをターゲットにする場合にランタイムサポートが無く、機能を使えない可能性があります。

03 今後の見通し

本提案は整数ジェネリックパラメータのコア機能を導入することに絞っており、その上に積み上げる拡張は別の Proposal に委ねられています。以下のような方向性が示されていますが、いずれも将来の構想であり、実現が約束されたものではありません。

固定サイズ・固定容量コレクション型

Vector<N, T> のような、サイズや容量を型パラメータで指定する標準ライブラリ型そのものの設計です。本提案はそうした型を書くための土台だけを提供し、具体的な型や API は別 Proposal で検討されます。

定数束縛のジェネリック引数としての利用

リテラルや既存のジェネリックパラメータだけでなく、let で宣言した定数もジェネリック引数として渡せるようにする案です。

static let bufferSize
    = MemoryLayout<Int8>.size * 64 + MemoryLayout<Int>.size * 8

var buffer = Vector<bufferSize, UInt8>(...)

ただし、型検査が式の評価結果に依存すると循環が生じるため、型検査器が定数の値を理解することは難しいと見込まれています。同じ書き方の式どうしは同一視できても、見た目の異なる 2 つの式が結果的に同じ値になるとしても、それらは異なる型として扱われる、といった制限が想定されています。

let fourShorts = 4 * MemoryLayout<Int16>.size
let eightBytes = 8 * MemoryLayout<Int8>.size

var v1: Vector<fourShorts, UInt8> = [...]
var v2: Vector<eightBytes, UInt8> = [...]
v1 = v2 // Error, different types

これは、宣言の異なる opaque result type が、動的には同じ型に解決されるとしても静的には別の型として扱われるのと似た発想です。

整数ジェネリックパラメータ間の算術

固定サイズ配列の連結のように、結果の長さが入力の長さの和になるなど、整数パラメータどうしの算術関係を型レベルで表現できるようにする案です。

func concat<let n: Int, let m: Int, T>(
    _ a: Vector<n, T>, _ b: Vector<m, T>
) -> Vector<n + m, T>

Swift の双方向の型推論との兼ね合いから、表現できる関係には制限が出ると見込まれています。

parameter pack の長さとの関連付け

variadic な parameter pack の「形」は最終的にその長さに帰着しますが、現状ではその長さを直接参照したり制約したりする手段がありません。整数ジェネリックパラメータをこの手段として使えるようにする案で、たとえば「整数パラメータが示すのと同じ個数の引数を取る」可変長 API を表現できる可能性があります。

struct Vector<let n: Int, T> {
    init(_ values: repeat each n * T)
}

Int 以外の値ジェネリックパラメータ

現時点では値ジェネリックパラメータの型は Swift.Int のみですが、将来的には任意の型の値を渡せるよう拡張する案です。本提案で let <name>: <Type> という構文を採用したのは、この拡張のための余地を残すためでもあります。

struct MatrixShape { var rows: Int, columns: Int }

struct Matrix<let shape: MatrixShape> {
    var elements: Vector<shape.rows, Vector<shape.columns, Double>>
}

任意型を許すと、型レベルでの等価性をどう判定するか、どのような構築・分解操作を型レベルで認めるかなど、考えるべきことが多く残ります。C++ の non-type template parameter や Rust の const generics に先例はあるものの、Swift では型のレイアウトを抽象化できることが重視されている点で事情が異なります。

整数 parameter pack

任意次元の行列のように、整数ジェネリックパラメータを可変長に取れるようにする案です。

struct MDMatrix<let each n: Int> { ... }

let mat2d: MDMatrix<4, 4> = ...
let mat4d: MDMatrix<120, 24, 6, 2> = ...