Swift 5.9でVariadic Genericsがやってくる
Swift 5.9がWWDC23で紹介され、マクロが大きな関心を呼んでいます。それと同時にVariadic Generics(可変長ジェネリクス)という新しい機能がやってきます。
可変長ジェネリクスは密かに新しいタイプのAPIを実現しています。この記事では可変長ジェネリクスについてじっくり解説します。
可変長ジェネリクス
普通、ジェネリクスは、固定個の型パラメータを持ちます。例えば、DEBUGビルドの場合にのみ値を出力する関数debugOnlyPrint
を作ることを考えてみます。ここではT
という1つの型パラメータが宣言されています。
func debugOnlyPrint<T>(_ value: @autoclosure () -> T) {
#if DEBUG
print(value())
#endif
}
// 使える
debugOnlyPrint("Hello, Swift")
型パラメータは複数あっても構いません。2つの引数を取るdebugOnlyPrint
を定義してみます。
func debugOnlyPrint<T, U>(_ value0: @autoclosure () -> T, _ value1: @autoclosure () -> U) {
#if DEBUG
print(value0(), value1())
#endif
}
// 使える
debugOnlyPrint("Hello, Swift", 3.14)
しかし、3つの引数に対応するにはまたもう1つ関数を定義する必要がありますし、4つに対応するにはさらにもう1つ必要です。こうなってくると、任意の個数の型パラメータを扱う方法が欲しくなってきます。
そこで導入されるのが可変長ジェネリクスです。可変長ジェネリクスを用いることで、任意の個数の型パラメータを扱うことができるようになります。
宣言
可変長ジェネリクスはeach
というキーワードで導入し、repeat
というキーワードで展開します。これだけだと何を言っているか分かりづらいですが、まずは可変長ジェネリクス版のdebugOnlyPrint
の宣言と呼び出し側を見てみましょう。
func debugOnlyPrint<each T>(_ value: repeat @autoclosure () -> (each T))
// 用例
debugOnlyPrint("Hi Swift", true, [1, 2, 3], -3.14, 256)
このeach T
は「型パック」と呼ばれる概念で、簡単に言えば型の列にあたります。用例においてはString, Bool, [Int], Double, Int
です。型パックそのものは型ではありません。
型パックを操作するために必要なのがrepeat
です。これは型パックを型パックへmap
する演算と言えば分かりやすいでしょう。repeat @autoclosure () -> (each T)
と書くことで、上記の型パックが@autoclosure () -> String, @autoclosure () -> Bool, @autoclosure () -> [Int], @autoclosure () -> Double, @autoclosure () -> Int
という異なる型パックへと変換されます。
計算
型パックで宣言された引数は「値パック」に入ります。たとえば、以下のように書くことでそれぞれのクロージャを実行し、結果を得ることができます。ここでもrepeat
が使われますが、こちらは値パックから値パックへのmap
だと考えることができます。
repeat (each value)() // "Hi Swift", true, [1, 2, 3], -3.14, 256
なお、値パックと型パックはまとめて「パラメータパック」と呼ばれることもあります。Appleによると、パラメータパックの命名では複数形ではなく単数形が好まれるようです。
制約
型パックにはいくつかの方法で制約をかけられます。まずは宣言時にプロトコルで制約をかける方法です。
func takeComparable<each T: Comparable>(...)
また、型パックのすべての型が同じ型であることを要請したい場合があります。この場合、where
とダミーの型パラメータを使えます。
// Elementはダミーの型パラメータになる
func maxes<each S: Sequence, Element>(sequence: repeat each S) -> (repeat (each S).Element) where repeat (each S).Element == Element
// each Sは[Int], Set<Int>、ElementはInt
maxes(sequence: [1, 2, 3], Set([832, 1, 42])) // (3, 832)
2つの型パックが同じ長さであるよう制約するには、単に同じ長さであることを必要とする型を用意します。
// TとUのタプルを取る場合、長さの制約は暗黙に推測される
func foo<each T, each U>(pair: repeat (each T, each U))
タプルとの関係
型パックとタプルは似ていますが、別物です。タプルは型としてさまざまな場面で利用できますが、型パックは言語機能としては露出していません。
しかし、型パックとタプル型、値パックとタプルの値を一緒に扱うのは容易です。実際、(repeat each value)
と書けばタプルになりますし、(repeat each T)
と書けばタプル型になります。
逆に、(repeat each value)
のようなタプルに対してrepeat
を適用することも可能です。
func take<each T>(tuple: (repeat each T)) {
debugOnlyPrint(repeat each tuple) // "Hi Swift", true, [1, 2, 3], -3.14, 256
}
// 用例
take(tuple: ("Hi Swift", true, [1, 2, 3], -3.14, 256))
可変長引数との違い
実は、「可変長引数」は今までのSwiftでも使えました。
func foo(values: Int...) {
print(values)
}
foo(values: 1, 2, 3, 4) // [1, 2, 3, 4]
...
を用いた可変長引数では、基本的に1つの型しか使えません。また、内部では配列として扱われます。可変長引数と言いつつ、実態は「配列をとる関数のシュガー」という状態でした。
例えばprint
は一見任意の型で可変長引数を取る関数ですが、その実態はAny...
です。
func print(_ values: Any..., separator: String = " ", terminator: String = "\n")
一方可変長ジェネリクスでは任意の型について、型の情報を落とさずに処理が可能になります。また、勝手に配列にされることがないので、例えばSet
などの異なるコレクションで扱いたい場合もより効率的に利用できます。最後に、inout
や@autoclosure
などの修飾子は可変長ジェネリクスでのみ扱えます。
可変長引数でできることは基本的に可変長ジェネリクスでもできるのですが、一点だけ違いがあります。可変長引数を取る関数はクロージャにしても可変長引数を取ることができます。一方で、可変長ジェネリックな関数は利用時には個別の長さ、型に解決されるため、個別のクロージャにすることしかできません。
func foo(values: Int...)
let closure: (Int...) -> () = foo // ok
// ok
closure(1, 2, 3, 4, ...)
func bar<T>(value: repeat each T) where repeat each T == Int
let closure1: (Int) -> () = bar // ok
let closure2: (Int, Int) -> () = bar // ok
let closure7: (Int, Int, Int, Int, Int, Int, Int) -> () = bar // ok
let closure: (Int...) -> () = bar // not ok
debugOnlyPrint
の実装
ただし、debugOnlyPrint
を完全に実装するのはもう少し骨が折れます。というのも、print
ではうまくいかないのです。
func debugOnlyPrint<each T>(_ value: repeat @autoclosure () -> (each T)) {
#if DEBUG
repeat print((each value)())
#endif
}
debugOnlyPrint(1, true, "Hi")
// 出力は「1 true Hi」を期待するが、実際は改行されてしまう
// 1
// true
// Hi
そこで、最初のパラメータを明示的に書き、さらにターミネーター(末尾につける文字)を変更します。
func debugOnlyPrint<First, each T>(_ firstValue: @autoclosure () -> First, _ value: repeat @autoclosure () -> (each T)) {
#if DEBUG
func printWithPrefixSpace<U>(value: U) {
print(" ", terminator: "") // 空白を出力
print(value, terminator: "") // 値を出力
}
print(firstValue(), terminator: "")
repeat printWithPrefixSpace(value: (each value)()) // 各値について「空白+値の文字列」を出力していく
print() // 最後の改行を出力する
#endif
}
debugOnlyPrint(1, true, "Hi")
// 出力は正しく「1 true Hi」となる
実際は少し複雑な実装になりましたが、それでも無数のオーバーロードを定義するよりはずっとマシになりました。
可変長ジェネリック型を作る
以上が基本的な可変長ジェネリクスの使い方ですが、さらに可変長ジェネリック型を作ることもできます。
struct VStack<each ContentView: View> {}
内部では関数の場合と同様にパックを利用してさまざまな関数を実装することができます。
実装テクニック
repeat
は基本的にmap
的な動作しか扱えませんが、mutating
な関数を実行することでreduce
的な処理を行うこともできます。
たとえば、可変長引数の個数を数える関数count
を作ってみます。
func count<each T>(value: repeat each T) -> Int {
var count = 0
func increment(trigger _: some Any) {
count += 1
}
repeat (increment(trigger: each value))
return count
}
ここでは、repeat
の中でeach value
を渡してincrement
関数を呼んでいます。ただしeach value
はどうでもよくて、呼び出し先で行われるcount += 1
が重要です。このように値を毎回1増やすことで、関数が呼ばれた回数分count
が増えていきます。その値を返してあげることで、可変超引数の個数を数えることができます。
この方法を使うとequal
のような関数も簡単に実装できます。
func equal<each T: Equatable>(left: repeat each T, right: repeat each T) -> Bool {
var result: Bool = true
func mutatingCompare<U: Equatable>(left: U, right: U) {
if !result {
return
}
result = left == right
}
repeat (mutatingCompare(left: each left, right: each right))
return result
}
ここではif !result
としていますが、throw
してしまう手もあります。
enum EqualError: Error { case notEqual }
func equal<each T: Equatable>(left: repeat each T, right: repeat each T) -> Bool {
func checkEqual<U: Equatable>(left: U, right: U) throws {
if left != right {
throw EqualError.notEqual
}
}
do {
repeat (try checkEqual(left: each left, right: each right))
} catch {
return false
}
return true
}
この辺りは色々デザインの余地がありそうです。
同様に、lessThan
のような関数を実装することもできます。
enum LessThanError: Error { case greaterOrEqual }
func lessThan<each T: Comparable>(left: repeat each T, right: repeat each T) -> Bool {
var result: Bool? = nil
func mutatingCompare<U: Comparable>(left: U, right: U, result: inout Bool?) {
if result != nil {
return
}
result = if left < right {
true
} else if left > right {
false
} else {
nil
}
}
repeat (mutatingCompare(left: each left, right: each right, result: &result))
return result == true
}
ここまで来ると、かなり便利な機能が実現できます。複数のクロージャを引数にとり、1つ目から順に評価してソートするsorted(with:)
を実装してみましょう。
public extension Sequence {
/// Returns a sorted array of the sequence's elements with the given closures.
/// ```swift
/// // you can path keyPaths (But now it is not recommended and can cause runtime crash. See the warning)
/// let result = array.sorted(with: \.firstKey, \.secondKey)
/// // you can also path closures
/// let result = array.sorted(with: { -$0.firstKey }, \.secondKey, \.thirdKey, { -$0.forthKey })
/// ```
/// - Parameter key: keys to sort on.
/// - Returns: A sorted array of the sequence's elements.
/// - Warning: This implementation in Swift 5.9 is working in a very fine balance with the compiler's mood. It seems there's a bug on compiler treatment of KeyPaths as functions, so currently it is not recommended to use KeyPaths on this function.
@inlinable
@_disfavoredOverload
func sorted<each T: Comparable>(with key: repeat (Element) -> each T) -> [Self.Element] {
func mutatingCompare<U: Comparable>(left: U, right: U, result: inout Bool?) {
if result != nil {
return
}
result = if left < right {
true
} else if left > right {
false
} else {
nil
}
}
return self.sorted { left, right in
var result: Bool? = nil
repeat (mutatingCompare(left: ((each key)(left)), right: ((each key)(right)), result: &result))
return result == true
}
}
}
// 用例
struct User {
var name: String
var age: Int
var joinedDate: Date
}
var users: [Users] = [...]
let result = users.sorted(with: {$0.name}, {$0.age}, {$0.joinedDate})
こういう感じで、少し裏技的ですが、便利な関数を実装していくこともできます。
既知の不具合
Swift 5.9はまだ正式リリース前です。Xcode 15 beta1現在、以下の制約があります。
-
@autoclosure
は呼び出し側でエラーが発生します - タプルに対する
repeat
が未実装です -
KeyPath
について可変長ジェネリクスが動作しません - 関連型による制約が動作しません
実はこの記事で紹介しているソースコードの一部も、Xcode 15 beta1で試したところ動かないものがありました。このため、仕様と照らし合わせておそらく言語側の不具合であろうと判断した上で、用例として掲載しています。
まとめ
以上で紹介したように、Variadic Genericsを用いることでより柔軟なAPIを作ることができます。Variadic Generics関係の機能は議論がまさに進行中の部分も多く、マクロとともに今後の進展が楽しみな分野ですね。
今回紹介したソースコードは以下にも掲載しています。いろいろ遊んでみているので、良かったら覗いてください。
Discussion
参考になります!
記事中の例で質問があります。
この最後の closure は別の例のタイポでしょうか?もしくは単純に不要な行だったりしますか?
ごめんなさい!Typoです。書きたかったのは
(Int...) -> ()
でした。修正しておきます!