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)

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 などは使えません(他の型への拡張は Future Directions)。

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 バージョンをターゲットにする場合にランタイムサポートが無く、機能を使えない可能性があります。

Future Directions(見通し)

本提案はコア機能の導入にとどめ、その上に積み上げる機能は別提案に委ねています。今後の方向性として次のようなものが挙げられていますが、いずれも speculative で実現が約束されたものではありません。

  • 固定サイズ・固定容量コレクション型: Vector<N, T> のような標準ライブラリ型そのものの設計。本提案はそれを書くための土台だけを提供します。
  • 定数束縛をジェネリック引数に使う: let bufferSize = ... として宣言した letVector<bufferSize, UInt8> のように渡せるようにする案。ただし型検査は式の評価に依存できないため、「見た目が同じ式どうしだけを同一視する」程度の扱いになる見込みです。
  • 型レベルの算術: Vector<n + m, T> のように、整数パラメータ間の足し算などを型で表現する案。双方向の型推論との兼ね合いで、表現できる関係には制限が出ると予想されています。
  • parameter pack の長さとの関連付け: variadic pack の「形」を整数ジェネリックパラメータで参照・制約する案。
  • Int 以外の値パラメータ: struct Matrix<let shape: MatrixShape> のように、任意の型の値をジェネリックパラメータにする案。let <name>: <Type> という構文は、この拡張のために残されています。
  • 整数 parameter pack: MDMatrix<let each n: Int> のように、可変長の整数ジェネリックパラメータを表現する案。