👶

[Swift5.7] ナウでヤングなジェネリクスを書く

2022/06/13に公開約4,000字

Swift5.7ではジェネリクス関連のさまざまな機能が追加されます。特にsomeを用いた軽量なジェネリクス構文を用いることで、既存のコードの可読性を大きく向上させることができます。

Swift5.7はXcode14以降で利用可能になる予定です。

1. <T: P>some Pで書く

以下のようなジェネリック関数を考えます。ここではまず、型パラメータ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は使えません。

2. <T: Sequence> where T.Element == Intを短く書く

次のようなジェネリック関数を考えます。ここではCollectionに準拠した型パラメータTが宣言され、さらにwhere節においてElementIntであることが示されています。

func sum<T: Collection>(_ values: T) -> Int where T.Element == Int {
    return values.reduce(0, +)   
}

Swift5.7では「ElementIntCollection」を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]
}

主要関連型はプロトコルごとに決まっていますが、標準ライブラリではCollectionElementのような直感的なものが選ばれています。

3. <T: Collection<Int>>をさらに短く書く

先ほど<T: Collection<Int>>を用いたコードを紹介しましたが、実はsomeと組み合わせることによってさらに簡潔に書くことが可能です。

func sum(_ values: some Collection<Int>) -> Int {
    return values.reduce(0, +)   
}

4. anyを使う

ジェネリクスとは少しずれますが、ついでなので覚えておきましょう。

今まで、「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のような型消去ラッパーと呼ばれる型とは機能が異なる点があります。AnyHashableHashableに準拠していますが、any Hashableは準拠していません。このため、例えば[any Hashable: Int]は許されず、[AnyHashable: Int]とする必要があります。一般的にany PPに準拠しません。

5. ジェネリック関数を呼び出すためならanyを使って良い

any PPに準拠していない」という点から、いくつかの制約がありました。

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 PPに準拠していませんが、「ジェネリック関数を呼び出す」という場面であればany Pを使えるようになりました。これは「存在型の暗黙的開放」という機能によるもので、ジェネリック関数を呼び出す際に暗黙的にany Pの実際の型の情報が取り出されるようになったことで実現しました。

func takeAnyCollectionOfInt(_ values: any Collection<Int>) {
    print(sum(values))    // Swift5.7ではコンパイルエラーにならない
}

この機能はエッジケースが多いため、依然細かい制約はあります。しかしこのようなケースではもうAnyCollectionを使う必要はありません。

まとめ

ナウでヤングなジェネリクスを書くコツは以下の4つです。

  • なるべく型パラメータを明示的に宣言せず、someを使う
  • 主要関連型を積極的に使う
  • 存在型のanyを明示的に書く
  • AnyPなどの型消去ラッパーを可能な場面ではany Pに置き換える

これらに従うことで可読性の高いジェネリクスを書くことができます。積極的に使っていきましょう。

Discussion

ログインするとコメントできます