Swift Digest
SE-0393 | Swift Evolution

値および型のparameter packs

Value and Type Parameter Packs

Proposal
SE-0393
Authors
Holly Borla, John McCall, Slava Pestov
Review Manager
Xiaodi Wu
Status
Implemented (Swift 5.9)

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

01 何が問題だったのか

Swiftのジェネリック関数は、型パラメータの数が固定でなければなりませんでした。そのため、引数の個数が可変で、かつ それぞれ異なる型を取りたい APIを書くには、次のような回避策しかありませんでした。

  • Any... などで型を消してしまう
  • 複数の型引数ではなく、ひとつのタプル型にまとめる
  • 引数の個数ごとに人為的な上限を設けてオーバーロードを並べる

典型的な例が、標準ライブラリのタプル比較演算子です。要素数ごとに個別のオーバーロードを用意する必要がありました。

func < (lhs: (), rhs: ()) -> Bool

func < <A, B>(lhs: (A, B), rhs: (A, B)) -> Bool
  where A: Comparable, B: Comparable

func < <A, B, C>(lhs: (A, B, C), rhs: (A, B, C)) -> Bool
  where A: Comparable, B: Comparable, C: Comparable

// 6要素のタプルまで同じパターンが続く

zip の複数系列版、タスクグループへの異種引数の流し込み、ビルダー系APIなど、ライブラリ側で「N個のジェネリック引数」を扱いたい場面は広く存在します。そのたびに手作業でオーバーロードを並べることは、ライブラリのメンテナンスコストを押し上げ、利用者にとっても「上限を超えたら急に型推論が通らなくなる」という分かりにくい体験を生んでいました。

この提案は、型パラメータと値パラメータの 個数そのもの を抽象化できる仕組みを言語に導入することで、これらのad hocな回避策を置き換えることを目的としています。これはSwiftにおけるvariadic genericsへの最初の一歩にあたり、今回のスコープはジェネリック関数(メソッド、イニシャライザ、subscriptを含む)に限られます。

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

型と値の両方について、「要素数が抽象的なリスト」を扱うための parameter pack を導入します。中心となる構文は次の2つです。

  • 宣言側: each を使って「パックである型パラメータ」を宣言する
  • 利用側: repeat + パターン式で、パックをリストへ展開する

型パラメータパックの宣言

ジェネリックパラメータリストで each T のように書くと、T はひとつの型ではなく「0個以上の型の並び」を表す 型パラメータパック になります。個々の要素に対する制約は通常のジェネリック制約と同じ構文で書けます。

// 'S' は各要素が 'Sequence' に適合する型パラメータパック
func zip<each S: Sequence>(...)

パック自体はファーストクラスの型や値ではありません。パックを使うには、パック展開(pack expansion) を通じて「型や値のリストが来る場所」に埋め込む必要があります。

パック展開 repeat

パック展開は repeat キーワードにパターンを続けた形をとります。パターンの中には、少なくともひとつの each T というパックへの参照が含まれていなければなりません。実行時には、置換されたパックの各要素ごとにパターンが繰り返され、その結果が周囲のリストに展開されます。

// 引数、戻り値、where 節でパック展開を使う
func zip<each S>(_ sequence: repeat each S) where repeat each S: Sequence

例えば SArray<Int>, Set<String> に置換されると、repeat Optional<each S>Optional<Array<Int>>, Optional<Set<String>> という2要素のリストに展開されます。

パック展開型(repeat P という型)を書けるのは、本質的に「型のリストを受け取る場所」に限られます。

  • 関数宣言や関数型のパラメータ位置: func foo<each T>(values: repeat each T)
  • タプル型のラベルなし要素: (repeat each T)

ラベル付きタプル要素でのパック展開は認められません。(t: repeat each T) を認めると、T := {Int, String} のときに (t: Int, String) となり、.t で最初の要素だけが取れてしまうといった直感に反する挙動になるためです。

値パラメータパック

関数パラメータの型をパック展開型にすると、その引数は 値パラメータパック になります。呼び出し側が渡した0個以上の引数が、値パックとしてまとめてバインドされます。

func tuplify<each T>(_ value: repeat each T) -> (repeat each T)

_ = tuplify()                 // T := {},              value := {}
_ = tuplify(1)                // T := {Int},           value := {1}
_ = tuplify(1, "hello", [0])  // T := {Int, String, [Int]}, value := {1, "hello", [0]}

値パックを使うには、これも repeat によるパック展開式の中に置きます。展開式はリストを受け取れる場所(呼び出しの引数、subscriptの引数、タプルリテラル、配列リテラルなど)で使えます。

func tuplify<each T>(_ t: repeat each T) -> (repeat each T) {
  return (repeat each t)
}

func forward<each U>(u: repeat each U) {
  _ = tuplify(repeat each u)                 // T := {repeat each U}
  _ = tuplify(repeat each u, 10)             // T := {repeat each U, Int}
  _ = tuplify(repeat each u, repeat each u)  // T := {repeat each U, repeat each U}
  _ = tuplify(repeat [each u])               // T := {repeat Array<each U>}
}

パターンの中で同じ添字の要素同士がまとめて使われる点に注目してください。複数のパックを同じパターン内で参照すると、自動的に「i番目の要素を組にする」ようなzip的な展開になります。

struct Pair<First, Second> {
  init(_ first: First, _ second: Second)
}

func makePairs<each First, each Second>(
  firsts first: repeat each First,
  seconds second: repeat each Second
) -> (repeat Pair<each First, each Second>) {
  return (repeat Pair(each first, each second))
}

let pairs = makePairs(firsts: 1, "hello", seconds: true, 2.0)
// pairs の型は (Pair<Int, Bool>, Pair<String, Double>)
// pairs の値は (Pair(1, true), Pair("hello", 2.0))

型マッチングと引数ラベル

呼び出しから型パラメータパックの置換を推論するために、2種類のマッチングルールが用意されています。

  • ラベルマッチング: 関数宣言の呼び出しでは、既存のvariadicパラメータと同じく「パック展開型のパラメータは最後に置くか、直後に必ずラベル付きのパラメータが来る」というルールでパックの切れ目を決めます。

    func concat<each T, each U>(t: repeat each T, u: repeat each U)
      -> (repeat each T, repeat each U)
    
    // T := {Int, Double}, U := {String, Array<Int>}
    concat(t: 1, 2.0, u: "hi", [3])
    

    次は曖昧なのでエラーになります。

    func bad<each T, each U>(t: repeat each T, repeat each U) // error
    
  • 型リストマッチング: それ以外(主にクロージャ式の文脈や、戻り値のタプル型からの推論)では、共通の先頭と末尾を剥がしたうえで、残った部分が「パック展開 vs 具体型のリスト」という組になっているときにだけマッチに成功します。

    func variadic<each T>(_: repeat each T) -> (Int, repeat each T, String) {}
    
    let fn = { x, y in variadic(x, y) as (Int, Double, Float, String) }
    // (Int, repeat each T, String) と (Int, Double, Float, String) から
    // T := {Double, Float} を推論
    

単一要素のパックが (repeat each T) というタプルを構成する場合、「1要素タプル」は作られず、自動的にスカラー型に剥がされます。例えば each T := {Int}(repeat each T) に代入すると型は Int になります。

メンバー型パラメータパック

型パラメータパックがプロトコル要件を持ち、そのプロトコルに関連型 A があれば、(each T).A も妥当なパターン型として使えます。これは各要素の関連型を取り出したパックを表します。

func variadic<each T: Sequence>(_: repeat each T) -> (repeat (each T).Element)

// T := {Array<Int>, Set<String>} のとき、戻り値の型は (Int, String)

ジェネリック要件の展開

既存の要件(適合、上位型、同型)も、repeat を付けた 要件展開 を通じてパックに対して書けます。

// 各要素が Sequence に適合する
func variadic<each S>(_: repeat each S) where repeat each S: Sequence { }

// 各要素の Element がすべて同じ型 T になる(same-element requirement)
func variadic<each S: Sequence, T>(_: repeat each S)
  where repeat (each S).Element == T { }

// 両辺が同じ長さのパックで、要素ごとに等しい(same-type-pack requirement)
func variadic<each S: Sequence, each T>(_: repeat each S)
  where repeat (each S).Element == Array<each T> { }

これらに加えて、コンパイラ内部では same-shape requirement(2つの型パラメータパックが同じ長さであること)が使われます。今回は明示的な構文は導入されませんが、関数の引数・戻り値・where 節に現れるパック展開からは自動的に推論されます。そのため、次のコードは「TU は同じ長さ」という制約が暗黙に課され、長さの違う呼び出しはエラーになります。

func zip<each T, each U>(
  firsts: repeat each T,
  seconds: repeat each U
) -> (repeat (each T, each U)) {
  return (repeat (each firsts, each seconds))
}

zip(firsts: 1, 2, seconds: "hi", "bye") // OK
zip(firsts: 1, 2, seconds: "hi")        // error: 長さが合わない

また、同型要件を使えば原理的にはパックの長さに複雑な制約(整数の一次方程式に相当)を書けてしまうため、この提案では、長さについては 抽象的なshape(パック単位の等値クラス) のみを許可し、具体的な長さや構造を混ぜる要件はコンフリクトとして診断されます。この制限は将来段階的に緩められる可能性があります。

オーバーロード解決

スカラーの型パラメータとパックの型パラメータは、同じ関数名でオーバーロードできます。パラメータパック版にスカラー版の引数列を「転送」可能な場合、スカラー版はパック版の サブタイプ として扱われ、通常どおりサブタイプが優先されます。

func overload() {}
func overload<T>(_: T) {}
func overload<each T>(_: repeat each T) {}

overload()      // 引数なしオーバーロードが呼ばれる
overload(1)     // スカラーオーバーロードが呼ばれる
overload(1, "") // パックオーバーロードが呼ばれる

この規則のおかげで、既存の固定個数オーバーロード群を将来パックオーバーロードに差し替えても、従来の呼び出しの解決結果は変わらないようになっています。

なお、この提案でvariadic genericsの対象になるのは、関数・メソッド・イニシャライザ・subscriptに限られます。型そのものを型パラメータパックでパラメータ化する仕組みは、別の提案で扱われます。

03 今後の見通し

この提案はvariadic genericsの最初の一歩であり、将来の拡張としていくつかの方向性が示されています。いずれも将来の構想であり、実現を約束するものではありません。

variadic genericsな型

今回は関数まわりだけが対象でしたが、structenumclass でも型パラメータパックを使えるようにする別提案が予定されています。これが入ると、要素数も型もまちまちなコレクション的なデータ構造を、ジェネリックな型として直接表現できるようになります。

ローカル変数としての値パック

値パックを関数パラメータだけでなくローカル変数としても宣言できるようにする方向性です。例えば次のように、パック展開型を持つ let を導入し、tt を別のパック展開式の中で使えるようにします。

func variadic<each T>(t: repeat each T) {
  let tt: repeat each T = repeat each t
}

明示的な型パック構文

現状、型パックは型マッチングによって暗黙に推論される存在で、{Int, String} のような「具体的な型パックそのもの」を書く構文はありません。将来的に、次のように型パックを明示的に書けるようにする案があります。

struct Variadic<each T> {}

extension Variadic where T == {Int, String} {} // {Int, String} は具体的な型パック

for-in によるパックの反復処理

パック展開式は要素ごとに同じパターンを評価しますが、途中で return するようなshort-circuitはできず、ステートメントを書きたい場合はクロージャや関数に切り出す必要があります。これを解消するために、値パックを for-in のソースとして展開できるようにする案があります。

func allEmpty<each T>(_ array: repeat [each T]) -> Bool {
  for a in repeat each array {
    guard a.isEmpty else { return false }
  }
  return true
}

ループ変数 a の型は、each T に課された要件を持つopaque型の Array で、i番目の繰り返しでは T のi番目の型パラメータが対応します。

パック要素のプロジェクション

パックを反復するのではなく、特定の位置の要素を取り出したい場面のために、添字アクセスでパック要素をプロジェクションする方向性も示されています。要素ごとに型が異なるため、2通りのアプローチが想定されています。

  • Int インデックスによる動的プロジェクション: 動的な型として要素を取り出し、opaque型を介して扱う。型消去やキャストを通じて関数の戻り値などにする。
  • キーパスによる静的プロジェクション: KeyPath や新しい PackIndex 型のような、パックを基底、要素を結果型としてパラメータ化されたインデックスで、静的に型付けされた要素アクセスを行う。

値の展開演算子

パック展開はいまのところ型パラメータパックと値パラメータパックにしか使えませんが、タプルや配列のように「0個以上の値の並び」を表す値にも展開演算子を適用できると便利です。スカラー値を受け取ってパック展開型の値を生む新しい式を導入する方向性が示されています。例えばタプルの要素をパックとして見るための(仮の).element 構文を使うと、次のように書けます。

func foo<each T, each U>(_ t: repeat each T, _ u: repeat each U) {}

func bar<each T, each U>(t: (repeat each T), u: (repeat each U)) {
  repeat foo(each t.element, each u.element)
}

パックの分解

パックの長さがコンパイル時にわかる場合に、先頭の要素と残りに分解する操作も検討されています。例えば、同型要件で (repeat each Element) == (First, repeat each Rest) のように書くことで、最初の要素の型と残りのパックを取り出します。

extension List {
  func firstRemoved<First, each Rest>() -> List<repeat each Rest>
    where (repeat each Element) == (First, repeat each Rest)
  {
    let (first, rest) = (repeat each element)
    return List(repeat each rest)
  }
}

タプルへの条件付き適合(tuple conformance)

ここまでの構想と、非nominal型に対するパラメータ化エクステンションの構文が組み合わされば、タプル型そのものに条件付きでプロトコル適合させる「tuple conformance」が表現できるようになります。例えば、要素がすべて Equatable のときにタプル自体を Equatable に適合させる、といった書き方が可能になります。

extension<each T: Equatable> (repeat each T): Equatable {
  public static func ==(lhs: Self, rhs: Self) -> Bool {
    for (l, r) in repeat (each lhs.element, each rhs.element) {
      guard l == r else { return false }
    }
    return true
  }
}