Array や Dictionary にもモダンでオシャレな extension を実装する


前書き

RxSwiftKingfisher、そして自分の Danbo などのライブラリーに採用されている .ex とかで拡張を明確に区別した extension の実装法、やり方についてはこちらの記事こちらの記事に詳しい実装方法を書かれていますが、それらの方法では ArrayDictionary といった自分自身の下位型縛りがある型にはそのまま適用できませんでした。where Base == Array をかくとコンパイラに怒られます。

昨日の投稿に引き続き、どうしても Eltaso[1, 2, 3].eltaso.random みたいな書き方をできるようにしたいと思い、昨日色々と四苦八苦した結果、どうにか無事成功したようで、やり方を共有しつつマサカリをお待ちしております。

目標

とりあえずこの記事ではより一般受けな書き方を共有したいと思いますので上記のとちょっと違い [1, 2, 3].ex.random を目標にしたいと思います。

前準備

一応こちらの記事をベースにしてますので、そちらの記事を参照しながらこの記事を読むとわかりやすいかもしれません。また、できればまず Array じゃない普通の型にこの実装をする方法を覚えておくことをオススメします。

実装

ExampleCompatible.swift
public protocol ExampleCompatible {
    associatedtype CompatibleType
    var ex: CompatibleType { get }
}

public struct Example<Base> {
    let base: Base
}

public struct ExampleWithSingleAssociatedType<Base, AssociatedType> {
    let base: Base
}
Array.swift
extension Array: ExampleCompatible {
    public var ex: ExampleWithSingleAssociatedType<Array<Element>, Element> {
        return ExampleWithSingleAssociatedType(base: self)
    }
}

extension ExampleWithSingleAssociatedType where Base == Array<AssociatedType> {
    public var random: AssociatedType? {
        guard !self.base.isEmpty else {
            return nil
        }
        let index = Int(arc4random_uniform(UInt32(self.base.count)))
        return self.base[index]
    }
}
Playground.swift
[1, 2, 3].ex.random // 2

解説

ExampleCompatible.swift

まず ExampleCompatible の部分についてみてわかる通り、Example<Base> の他に、ExampleWithSingleAssociatedType<Base, AssociatedType> というやつを新たに追加しました。通常の Example と違い、こちらは BaseAssociatedType の2つの型縛りがあります。それ以外は Example と同じです

また、ここであえて ExampleCompatibleextension を削除しました。なぜかというと ArrayExampleWithSingleAssociatedType に入れますが、通常の型は Example に入れるので、直接 extension ExampleCompatible の中に ex を定義してあげると物によっては違う型の ex が返されてくる危険性がありますので、次の Array.swift の部分と同じように、直接型拡張の中に定義することにしました。

Array.swift

次に Array.swift の中ですが、まずは Array を拡張して ExampleCompatible を適用させます。これは通常の型の拡張を作る時と同じです。ただしここでは直接 ex の戻り値を定義する必要があります。戻り値の型はご覧の通り ExampleWithSingleAssociatedType<Array<Element>, Element> です。つまり ExampleWithSingleAssociatedTypeBaseArray<Element>AssociatedTypeArray.Element になります。

ここまで定義できたらあとは通常の型の時の Example の拡張と同じように、ExampleWithSingleAssociatedType を拡張します。制約は Base == Array<AssociatedType> になります。

拡張の中に random: AssociatedType? というプロパティーを定義します。中身は単純な自分のランダムな要素を返す(ただし空配列の場合は nil を返す)だけです。

Playground.swift

ここまでできたらもう Playground 開いて [1, 2, 3].ex.random を書けば自動で 123 の中からランダムな数を返してくれる命令が使えます。ね?簡単でしょ?

Array 以外への適用

上記の ExampleWithSingleAssociatedType の定義からわかる通り、BaseAssociatedType の二つの型縛りがあるので、Array だけでなく、自分自身に下位の型縛りが一つある型なら基本どれにでも適用できます。例えば Range<Bound> とかですね。書き方は単純に Array.swift の中身の ArrayRangeElementBound に書き換えればできるはずです。

では、下位型縛りが二つある物、例えば Dictionary<Key, Value> はどうすればいいかというと、これも非常に単純で、さっき ExampleWithSingleAssociatedType<Base, AssociatedType> を追加したのと同じように、もう一つ ExampleWithDualAssociatedType<Base, AssociatedType1, AssociatedType2> という struct を定義して、DictionaryexExampleWithDualAssociatedType<Dictionary<Key, Value>, Key, Value> で返せば OK です。まあ型縛りが増えてくると書くのがだるいですねw

余談

上記はより一般向け(?)な書き方で書きましたが、自分の Eltaso では Example ではなく EltasoContainer で名付けられてたり、拡張は eltaso の名前にしてます。せっかくわざわざ名前衝突を回避しようとこんな書き方導入したっていうのに、他の同様な書き方をするライブラリー導入して ex で名前衝突したらシャレにならないですもんね。というわけで皆さんも自分でこの書き方を導入する際は ex ではなく他の名前で入れた方が無難かと思います。

ちなみにこの拡張方法は Eltaso の 4.0 で一般公開する予定ですが、このバージョンではこれと別にメソッドチェーンの対応も実装しようと考えております(どれほどの実用性があるかはさておき)。仕事量がかなり多いのでもしかすると 4.1 で実装する可能性もありますが、一応現在の対応状況をチョイ見せするとこんな感じです:

パフォーマンス面も思ったよりそんなに悪くないようです:

たーのしー