[Swift5.7] ナウでヤングなジェネリクスを書く
Swift5.7ではジェネリクス関連のさまざまな機能が追加されます。特にsome
を用いた軽量なジェネリクス構文を用いることで、既存のコードの可読性を大きく向上させることができます。
Swift5.7はXcode14以降で利用可能になる予定です。
<T: P>
をsome P
で書く
1. 以下のようなジェネリック関数を考えます。ここではまず、型パラメータT
を宣言し、次にそれがNumeric
に準拠していることを示します。さらに引数リストにおいてvalue
の型をT
と指定します。
func printDouble<T: Numeric>(_ value: T) {
print(value * 2)
}
some
を用いると、上記のコードを以下のように書くことができます。ここでは型パラメータの宣言は行われず、代わりに引数リストのvalue
の型がsome Numeric
になりました。このsome Numeric
は「Numeric
に準拠したある特定の型」と言った意味合いで、振る舞いは型パラメータを使った場合と全く同じです。
func printDouble(_ value: some Numeric) {
print(value * 2)
}
残念ながら、some P
が使えない場面もあります。例えば次のような状況です。ここでは型パラメータT
が2回使われています。
func swapNumeric<T: Numeric>(_ lhs: inout T, rhs: inout T) {
(lhs, rhs) = (rhs, lhs)
}
このような状況ではsome
を使うことはできません。引数リストの2つのsome
は同じ型とは見做されないからです。だから、some
を使ってswapNumeric
を下のように書いた場合
func swapNumeric(_ lhs: inout some Numeric, rhs: inout some Numeric) {
(lhs, rhs) = (rhs, lhs)
}
解釈はこうなってしまいます。
func swapNumeric<T: Numeric, U: Numeric>(_ lhs: inout T, rhs: inout U) {
(lhs, rhs) = (rhs, lhs)
}
一般的に、同じ型パラメータを2度以上用いるような関数はsome
を使って書くことができません。また、ジェネリック型の型パラメータにもsome
は使えません。
<T: Sequence> where T.Element == Int
を短く書く
2. 次のようなジェネリック関数を考えます。ここではCollection
に準拠した型パラメータT
が宣言され、さらにwhere
節においてElement
がInt
であることが示されています。
func sum<T: Collection>(_ values: T) -> Int where T.Element == Int {
return values.reduce(0, +)
}
Swift5.7では「Element
がInt
のCollection
」をCollection<Int>
と書けるようになりました。そこで次のように書き換えることができます。where
節がすっきりなくなり、非常にきれいになりました。
func sum<T: Collection<Int>>(_ values: T) -> Int {
return values.reduce(0, +)
}
これはSwift5.7で追加された「主要関連型」という機能によるものです。これによってプロトコルの関連型のうち「特に重要なもの」をジェネリクスのように表記できるようになりました。
Swift5.7でのCollection
は次のように宣言されています。
protocol Collection<Element> {
associatedtype Element
// ...
}
このため、例えばCollection
の別の関連型であるIndex
を参照するには、引き続きwhere
節を用いる必要があります。
func first<T: Collection>(_ values: T) -> [Int] where T.Index == Int {
return values[0]
}
主要関連型はプロトコルごとに決まっていますが、標準ライブラリではCollection
のElement
のような直感的なものが選ばれています。
<T: Collection<Int>>
をさらに短く書く
3. 先ほど<T: Collection<Int>>
を用いたコードを紹介しましたが、実はsome
と組み合わせることによってさらに簡潔に書くことが可能です。
func sum(_ values: some Collection<Int>) -> Int {
return values.reduce(0, +)
}
any
を使う
4. ジェネリクスとは少しずれますが、ついでなので覚えておきましょう。
今まで、「Codable
に準拠しているならなんでもいい」というような型(存在型)を書くときには、単にCodable
と書いていました。
func foo(_ value: Codable) {}
Swift5.6からは新しくany
というキーワードが追加され、この関数を次のように書くことが奨励されています。
func foo(_ value: any Codable) {}
この記法に既存の記法との動作の違いはありません。Swift6以降では上のコードをコンパイルエラーとすることが予定されているので、積極的にany
を付けましょう。
このany
は主要関連型とも組み合わせることができます。
func takeAnyCollectionOfInt(_ values: any Collection<Int>) {
print(values.map {$0 * 2})
}
注意が必要な点に、AnyHashable
のような型消去ラッパーと呼ばれる型とは機能が異なる点があります。AnyHashable
はHashable
に準拠していますが、any Hashable
は準拠していません。このため、例えば[any Hashable: Int]
は許されず、[AnyHashable: Int]
とする必要があります。一般的にany P
はP
に準拠しません。
any
を使って良い
5. ジェネリック関数を呼び出すためなら「any P
はP
に準拠していない」という点から、いくつかの制約がありました。
func sum(_ values: some Collection<Int>) -> Int {
return values.reduce(0, +)
}
func takeAnyCollectionOfInt(_ values: any Collection<Int>) {
print(sum(values)) // `any P`は`P`に準拠していないため、コンパイルエラーになってしまう
print(sum(AnyCollection(values))) // AnyCollectionで型を消去してから使う必要がある
}
Swift5.7でもany P
はP
に準拠していませんが、「ジェネリック関数を呼び出す」という場面であればany P
を使えるようになりました。これは「存在型の暗黙的開放」という機能によるもので、ジェネリック関数を呼び出す際に暗黙的にany P
の実際の型の情報が取り出されるようになったことで実現しました。
func takeAnyCollectionOfInt(_ values: any Collection<Int>) {
print(sum(values)) // Swift5.7ではコンパイルエラーにならない
}
この機能はエッジケースが多いため、依然細かい制約はあります。しかしこのようなケースではもうAnyCollection
を使う必要はありません。
まとめ
ナウでヤングなジェネリクスを書くコツは以下の4つです。
- なるべく型パラメータを明示的に宣言せず、
some
を使う - 主要関連型を積極的に使う
- 存在型の
any
を明示的に書く -
AnyP
などの型消去ラッパーを可能な場面ではany P
に置き換える
これらに従うことで可読性の高いジェネリクスを書くことができます。積極的に使っていきましょう。
Discussion