メソッドでジェネリクスの引数にデフォルト値を指定する方法(Swift)

## はじめに

メソッドでジェネリクスの引数にデフォルト値を指定する方法を紹介します。

## 環境

- OS：macOS Monterey 12.2.1
- Xcode：13.3.1 (13E500a)
- Swift：5.6

## 注意

- 可読性の観点から、本記事では存在型 [^existential-type] に必ず `any` キーワード [^any] を付けます

[^existential-type]: 英語で「existential type」という。プロトコルを型として使う場合の型のこと。
[^any]: Swift 5.6で追加されたキーワード。存在型を示す。

## 問題

`FooRepository` プロトコルに準拠した `FooClient` クラスがあるとします。

```swift: FooRepository.swift
protocol FooRepository {
    func foo() -> String
}
```

```swift:FooClient.swift
final class FooClient {
    static let shared = FooClient()

    private init() {}
}

extension FooClient: FooRepository {
    func foo() -> String {
        "Foo"
    }
}
```

プロトコルを型として使う場合、メソッドの引数にデフォルト値を指定するのは簡単です。

```swift:FooInteractor.swift
final class FooInteractor {
    private let fooRepository: any FooRepository

    init(fooRepository: any FooRepository = FooClient.shared) { // !!!: 簡単にデフォルト値を指定できる
        self.fooRepository = fooRepository
    }
}
```

しかしプロトコルを制約として使う場合、以下の書き方だとビルドエラーになります。

```diff_swift:FooInteractor.swift
final class FooInteractor {
    private let fooRepository: any FooRepository

-   init(fooRepository: any FooRepository = FooClient.shared) {
+   init<FR: FooRepository>(fooRepository: FR = FooClient.shared) { // FIXME: ビルドエラーになる
        self.fooRepository = fooRepository
    }
}
```

```:ビルドエラー
default argument value of type 'FooClient' cannot be converted to type 'FR'
```

「なんでビルドエラーになるの？」と思いましたが、おそらく `FR` に `FooClient` 以外のクラスを指定した場合に型の不一致が発生するためです。

```swift:❌型の不一致が発生する
final class BarClient {
}

extension BarClient: FooRepository {
    func foo() -> String {
        "Bar"
    }
}

let interactor = FooInteractor<BarClient>() // FIXME: `BarClient` と `FooClient` は型が異なる
```

よく考えればビルドエラーが発生するのは当たり前ですが、なかなか気づけませんでした。

ではどうすればこの問題を解決できるでしょうか？

## 解決策

解決策をいくつか紹介します。

### 制約からデフォルト値を決定する

型の不一致を発生させなければいいので、 `FooClient.shared` を `FR.shared` に変更すれば解決できます。

```diff_swift:FooInteractor.swift
- final class FooInteractor {
-   private let fooRepository: any FooRepository
+ final class FooInteractor<FR: FooRepository> {
+   private let fooRepository: FR

-   init(fooRepository: any FooRepository = FooClient.shared) {
+   init(fooRepository: FR = FR.shared) {
        self.fooRepository = fooRepository
    }
}
```

これに合わせてプロトコルとクラスも修正します。

```diff_swift: FooRepository.swift
protocol FooRepository {
+   static var shared: Self { get }
    func foo() -> String
}
```

```diff_swift:FooClient.swift
final class FooClient {
-   static let shared = FooClient()
-
    private init() {}
}

extension FooClient: FooRepository {
+   static let shared = FooClient()
    func foo() -> String {
        "Foo"
    }
}
```

これで引数の型とデフォルト値の型が必ず一致し、ビルドに成功します。

ただ `shared` の実装を強制するので、好みは分かれます。

https://twitter.com/_take_hito_/status/1514010894833168385

あとMockolo [^mockolo] は `Self` を返すコンピューテッドプロパティに対応していませんでした。
取り急ぎIssueを作成しましたが、現状はメソッドを使うなどの回避策を取る必要があります。

[^mockolo]: モック生成ツール。https://qiita.com/uhooi/items/0226a28190aea6392806

https://github.com/uber/mockolo/issues/185

### デフォルト値を持つメソッドを追加する

デフォルト値を持たないメソッドを用意し、それとは別にデフォルト値を持つメソッドを実装することでも解決できます。

```diff_swift:FooInteractor.swift
- final class FooInteractor {
-   private let fooRepository: any FooRepository
+ final class FooInteractor<FR: FooRepository> {
+   private let fooRepository: FR

-   init(fooRepository: any FooRepository = FooClient.shared) {
+   init(fooRepository: FR) {
        self.fooRepository = fooRepository
    }
}

+ extension FooInteractor where FR == FooClient {
+   convenience init() {
+       self.init(monstersRepository: .shared)
+   }
+ }
```

今回はイニシャライザなので `convenience` を付けていますが、通常のメソッドなら単純に別メソッドを追加するのみです。

コードが冗長になりますが、 `shared` の実装を強制したくない場合に有用です。

ちなみにエクステンションを使わずに実装することもできます。

```diff_swift:FooInteractor.swift
final class FooInteractor<FR: FooRepository> {
    private let fooRepository: FR

    init(fooRepository: FR) {
        self.fooRepository = fooRepository
    }
+
+   convenience init() where FR == FooClient {
+       self.init(monstersRepository: .shared)
+   }
}

- extension FooInteractor where FR == FooClient {
-   convenience init() {
-       self.init(monstersRepository: .shared)
-   }
- }
```

処理は同じなので、どちらで実装するかは好みです。
私は今回のようにイニシャライザを1つ追加するくらいならエクステンションを使わず、メソッドを複数実装するなど長くなる場合はエクステンションでまとめるのが好みです。

### 型消去する

Twitterで教えていただいた方法です。

https://twitter.com/noppefoxwolf/status/1513900771703623681

`FooRepository` を型消去 [^type-erasure] しても解決できます。

[^type-erasure]: 英語で「type erasure」という。型変数が存在する型から型変数を消去すること。https://ja.m.wikipedia.org/wiki/型消去

まず型消去型 [^type-erased-type] を用意します。
型消去型は `Any○○` と命名されることが多いです。
Swiftにはデフォルトで [AnyHashable](https://developer.apple.com/documentation/swift/anyhashable) や [AnySequence](https://developer.apple.com/documentation/swift/anysequence) などの型消去型が用意されています。

[^type-erased-type]: 英語で「type-erased type」という。型消去された型のこと。

```swift:AnyFooRepository.swift
final class AnyFooRepository {
    private let _foo: () -> String

    init(_ base: any FooRepository) {
        _foo = { base.foo() }
    }
}

extension AnyFooRepository: FooRepository {
    func foo() -> String {
        _foo()
    }
}
```

プロトコルによって必要なプロパティやメソッドがわかっているため、イニシャライザではそれをプロパティ（今回はないですが）やクロージャに保持しています。
今回は `base` インスタンスごと保持するのでも問題ありませんが、 `associatedtype` を持つプロトコルは存在型として使えない [^casting-to-protocol-having-associatedtype] ため、このような方法を取ることが多いようです。

[^casting-to-protocol-having-associatedtype]: https://medium.com/finc-engineering/casting-to-protocol-having-associatedtype-e5854994a97f

https://twitter.com/_take_hito_/status/1514087824613834754

あとは存在型の代わりに型消去型を使うようにすればOKです。

```diff_swift:FooInteractor.swift
final class FooInteractor {
-   private let fooRepository: any FooRepository
+   private let fooRepository: AnyFooRepository

    init(fooRepository: any FooRepository = FooClient.shared) {
-       self.fooRepository = fooRepository
+       self.fooRepository = AnyFooRepository(fooRepository)
    }
}
```

`FooInteractor` に型を焼かず [^type-parameter] に済みますが、イニシャライザの引数に存在型が残ります。

今回だと型消去を使わなくても解決できるので、メリットがあまりないかもしれません。

[^type-parameter]: クラスや構造体、列挙型に型引数を付けることを指す俗語。

https://twitter.com/noppefoxwolf/status/1514055604998144002

## おまけ

### 将来は元のコードのまま書けるようになる

[コメント](https://qiita.com/uhooi/items/336935761a631f5ea9be#comment-2e707a46fe15c936abae) で教えていただいた通り、将来は元のコードのビルドが通るようになります。

```swift:FooInteractor.swift
final class FooInteractor {
    private let fooRepository: any FooRepository

    init<FR: FooRepository>(fooRepository: FR = FooClient.shared) { // !!!: 将来はビルドエラーにならなくなる
        self.fooRepository = fooRepository
    }
}
```

詳細は以下のプロポーザルをご参照ください。

https://github.com/apple/swift-evolution/blob/main/proposals/0347-type-inference-from-default-exprs.md

Acceptされたので、実装されるのはそう遠くないと思います。

### どの解決策が好み？

どの解決策が好みか、Twitterでアンケートを取ってみました。

https://twitter.com/the_uhooi/status/1513818553199525888

#### デフォルト値を指定しない

「そもそもデフォルト値を指定しない」という意見も頂きました。

https://twitter.com/mtj_j/status/1513819916054409219?s=20&t=QaG1ynizbJmkx_bfFoBpKw

まさにその通りですが、単体テストでモックに差し替えたいだけの場合はバスタードインジェクションが便利なんですよね…。

#### インスタンスの生成をやめ、すべてstaticメソッドにする

「単体テストでモックに差し替えるのが目的、かつシングルトンなら、インスタンスを生成せずにすべて `static` メソッドにする」という意見も頂きました。

https://twitter.com/koher/status/1514138781049241604

型引数でDIする発想はなかったので勉強になりました。

具体的な実装方法は以下の記事をご参照ください。

https://qiita.com/uhooi/items/16aa67b44e2614c8d7b9

## おわりに

これでメソッドの引数がジェネリクスでもデフォルト値を指定できるようになりました！

他にも解決策や意見などありましたら、コメントなどで教えていただけると嬉しいです :relaxed:

## 参考リンク

- https://twitter.com/the_uhooi/status/1512971658222792706
- https://github.com/uhooi/UhooiPicBook/pull/327/commits/5972e164a10b33364496e1ddeba812e29f50889b
- https://github.com/uhooi/UhooiPicBook/pull/327/commits/a866bf72ae8b5f9899d2b08754de0d7f835a66a9
- https://gist.github.com/noppefoxwolf/0b9551a394ec26d6987821bd0620a32b
- https://gist.github.com/Koshimizu-Takehito/26a5213b514542660bf32521a9b0b336
- [[Swift] AnyPokemonリターンズ ~型消去型の弁明~ - Qiita](https://qiita.com/ysn/items/5ccf14386c27f421e252)