第 2 章  Protocol-oriented Programming

プロトコルとリバースジェネリクス

本章では、制約としてプロトコルを用いることで、 値型 中心のコードにおいてもパフォーマンスを損ねずにコードを抽象化できることを見てきました。しかし、 Swift の誕生から時間が経過し、様々なユースケースが生じる中で、制約としてのプロトコルによるコード抽象化に欠けているものが明らかになってきました。

そのような問題と解決策について Core Team の Joe Groff さんがまとめたドキュメントが “Improving the UI of generics” です。このドキュメントの中で リバースジェネリクス という新しい概念が説明され、その簡易形である Opaque Result Type が Swift 5.1 で部分的に導入されました。

本節では、制約としてのプロトコルと リバースジェネリクス および Opaque Result Type の関係を説明し、プロトコルを使ったコード抽象化の全体像を示します。

制約としてのプロトコルに欠けていた抽象化

前節まで見てきたように、 Swift にとっては制約としてのプロトコルが適しています。実際、 Swift の標準ライブラリでも制約としてのプロトコルが広く使われており、ほとんどすべてのプロトコルが制約として使用されています。

たとえば、 Sequence プロトコルでは IteratorProtocolassociatedtype の制約として使用されています。

protocol Sequence {
    associatedtype Iterator: IteratorProtocol // 制約として使われている
    func makeIterator() -> Iterator
}

興味深いことに、 Kotlin では同様の目的でインタフェース( Swift のプロトコルのような役割を果たすもの)が iterator メソッド( Swift の makeIterator メソッドに相当)の戻り値の型として使用されています。

// Kotlin
interface Iterable<out T> {
    operator fun iterator(): Iterator<T> // 型として使われている
}

Swift では IteratorProtocol が制約として使われ、 Kotlin では Iterator インタフェースが型として使われているわけです。これは、両言語の特徴をよく表しています。 Kotlin は参照型中心の言語なので、 Iterator インタフェースを型として使ってもオーバーヘッドは大きくありません。しかし、値型中心の Swift ではそうはいきません。

そもそも、 IteratorProtocolElement という associatedtype を持っているので、現状の Swift では型として使うことができません。しかし、 Generalized Existential がサポートされれば IteratorProtocol も型として使うことが可能になります。

// IteratorProtocol を型として使う場合
protocol Sequence {
    associatedtype Element
    func makeIterator() ->
        IteratorProtocol<.Element == Element>  // 型として使われている
}

Sequence プロトコルが上記のように宣言されれば、 Kotlin の Iterable インタフェースとほぼ同じ内容になります。

しかし、たとえ Generalized Existential がサポートされても、 makeIterator メソッドの戻り値を IteratorProtocol 型にするのは望ましくありません。もし IteratorProtocol を型として使うと、イテレータの next メソッドを使って要素を取り出す度に Existential Container のオーバーヘッドが発生します。イテレーションのように繰り返し実行される基本的な処理において Existential Container のオーバーヘッドを許容することはできません。そのため、 Swift では IteratorProtocol を制約として用いることで、 Existential Container のオーバーヘッドを防止しています。

たとえば、 Array の場合、 makeIterator メソッドの戻り値の型は IndexingIterator<[Element]> という具体的な型になります。

extension Array: Sequence {
    func makeIterator() -> IndexingIterator<[Element]> { ... }
}

同様に String の場合には makeIterator メソッドの戻り値の型は String.Iterator になります。

extension String: Sequence {
    func makeIterator() -> String.Iterator { ... }
}

IteratorProtocolIterator という associatedtype の制約としてのみ現れ、 ArrayString などの具体的な型を実装する際には IndexingIterator<[Element]>String.Iterator という具体的な型に置き換えられるわけです。そうすると、これらのイテレータを利用する際にも、抽象的な IteratorProtocol 型としてではなく、それぞれの具体的なイテレータ型として利用することができ、 Existential Container のオーバーヘッドが発生しません。 next メソッドの呼び出しもオーバーヘッドのない 静的ディスパッチ になります。

しかし、未解決の問題が一つあります。

前述のように、 ArraymakeIterator メソッドは IndexingIterator<[Element]> を返し、 StringmakeIterator メソッドは String.Iterator を返します。しかし、これが露出していることは必ずしも望ましくありません。

Swift の標準ライブラリには次のように、 IteratorProtocol に適合する大量のイテレータ型が存在します。

IteratorProtocol に適合する型を実装する際には、これらのイテレータ型を使い分けて、もしくは独自のイテレータ型を実装して返さなければなりません。それ自体は問題ではなく、 IteratorProtocol が「型として」・「制約として」どちらの方法で使われていたとしても必要なことです。

問題となるのはイテレータの利用時です。 makeIterator メソッドの利用者はこれらのイテレータ型の違いを意識する必要はありません。

var iterator: IndexingIterator<[Int]> // この型を意識する必要はない
    = array.makeIterator()

この変数 iterator の型が IndexingIterator<[Int]> だろうと、 Int を取り出す他の何らかのイテレータであろうと、その違いを利用者が意識することはありません。前述のほとんどのイテレータ型は next メソッドしか持たず、 Sequence から要素を取り出すという意味でどれも同じ機能を提供しています。

にも関わらず、 ArraymakeIterator メソッドは戻り値の型 IndexingIterator<[Int]> を公開しています。もし将来的に Array に特化したより高速なイテレータが実装されたとしても、 ArraymakeIterator メソッドの戻り値の型を変更するのは困難です。 ArraymakeIterator メソッドは Swift 標準ライブラリの public な API であり、その型を変更するということは、標準ライブラリの API の型を変更するということだからです。

もし、 ArraymakeIterator メソッドの戻り値の型が次のように抽象化されているとどうなるでしょうか。

extension Array: Sequence {
    func makeIterator() -> Elementを取り出す何らかのイテレータ { ... }
}

これであれば、実体として IndexingIterator<[Element]> が返されていたところを、 Array に特化したより高速なイテレータに差し替えられても API としての表面上の型に変更はありません。 利用者にとって本来必要なのはこのレベルの抽象度です。 具体的なイテレータの型を知る必要はありません。

しかし、かといって戻り値の型に Generalized Existential を使う( IteratorProtocol を型として使う)わけにはいきません。

extension Array: Sequence {
    func makeIterator() -> IteratorProtocol<.Element == Element> { ... }
}

今の Swift ではこれはできませんが( Generalized Existential がサポートされていないことに加えて、 Self-conformance もサポートされていないので、 IteratorProtocol<.Element == Element>associatedtype Iterator: IteratorProtocol を満たせない)、仮にできたとしても Existential Container のオーバーヘッドの問題が残ります。 makeIterator メソッドの戻り値の型は抽象的に書きたいですが、抽象化のために Existential Container のオーバーヘッドを受け入れることはできません。

つまり、今望んでいるのは、 抽象的にコードを書きながら、具象型と同じパフォーマンスがほしい ということです。

抽象的なコードと具象型のパフォーマンス(引数の場合)

似たような話が前にもありました。ジェネリック関数です。プロトコルを制約として使ってコードを抽象化することで、具象型と同等のパフォーマンスを得ることができました。

// 抽象型引数と具象型のパフォーマンス
func useAnimal<A: Animal>(_ animal: A) {
    print(animal.foo())
}

上記のコードでは引数 animal の型が型パラメータ A によって抽象化されていますが、コンパイル時に型パラメータが 特殊化 されることによって、具象型で記述したのと同等のパフォーマンスが得られました。たとえば、上記の useAnimal 関数の型パラメータ ACat特殊化 されると、下記の useAnimal 関数と同等のパフォーマンスを実現できます。

// 具象型引数
func useAnimal(_ animal: Cat) {
    print(animal.foo())
}

これと同じようなアプローチで、 makeIterator メソッドにおいても抽象的なコードと具象型のパフォーマンスを両立できないでしょうか。

抽象的なコードと具象型のパフォーマンス(戻り値の場合)

makeIterator メソッドと先の useAnimal 関数の違いは、抽象化するのが戻り値の型なのか引数の型なのかという点です。ただ、 makeIterator メソッドは useAnimal 関数と違って複雑です。関数ではなくメソッドですし、プロトコルによって宣言されています。また、 IteratorProtocolassociatedtype である Element も関係しています。まずはよりシンプルな例を考えてみましょう。

useAnimal 関数は、引数として Animal を受け取ります。それとの対比として、戻り値として Animal を返す makeAnimal 関数を考えてみましょう。まずは、最もシンプルに具象型 Cat を返す makeAnimal 関数を考えます。

// 具象型戻り値
func makeAnimal() -> Cat {
    Cat()
}

先の useAnimal 関数のように、これをジェネリック関数にして戻り値の型を抽象化できないでしょうか。しかし、次のようなジェネリック関数にしようとするとコンパイルエラーになってしまいます。

// 抽象型戻り値と具象型のパフォーマンス?🤔
func makeAnimal<A: Animal>() -> A {
    Cat() // ⛔コンパイルエラー
}

なぜなら、ジェネリクスの型パラメータは必ずその API の利用者が決定するからです。 useAnimal 関数であれば、関数の利用者が引数に Cat 型の値を渡すことによって、型パラメータ ACat に決定されます。

useAnimal(Cat()) // 利用者が A を Cat に決定

そして、抽象的な型 A を利用するのは useAnimal 関数の実装者です。

func useAnimal<A: Animal>(_ animal: A) {
    print(animal.foo()) // 実装者が A を使用
}

しかし、 makeAnimal 関数ではこの関係が逆転します。 makeAnimal 関数が Cat 型を値を返すことを決定するのは関数の実装者です。

func makeAnimal() -> A { // A はどのように宣言する?🤔
    Cat() // 実装者が A を Cat に決定
}

そして、 makeAnimal 関数の利用者は戻り値の型を抽象的な型 A として扱います。

let animal = makeAnimal() // 利用者が A を使用

上記の関係をまとめると次のようになります。

useAnimal利用者が具象型を決定する。実装者が抽象型を使用する。
makeAnimal実装者が具象型を決定する。利用者が抽象型を使用する。

useAnimalmakeAnimal利用者実装者 が逆転しています。 makeAnimal の戻り値の型を抽象化することは通常のジェネリクスではできません。

これを可能にするものとして、 リバースジェネリクス という概念が Manolo van Ee さんによって提唱されました。また、 Core Team の Joe Groff さんが関連する議論を整理し、 “Improving the UI of generics” というドキュメントにまとめました。このドキュメントは、過去にジェネリクス回りのロードマップとして示された “Generics Manifesto” を補完する位置づけのものです。 Swift 5.1 時点では リバースジェネリクス は採択されていません。しかし、機能的に リバースジェネリクス のサブセットと言える Opaque Result Type (後述)は Swift 5.1 で部分的にサポートされました。そのような経緯から、 リバースジェネリクス は将来的に何らかの形で採択される可能性が高いと筆者は考えています。

ここでは、 リバースジェネリクス について議論されている中で最も有力な次のシンタックスを採用します。通常のジェネリクスと異なり、 リバースジェネリクス では型パラメータを -> の後ろに記述します。

func makeAnimal() -> <A: Animal> A {
    Cat()
}

この makeAnimal 関数は次のように使えます。

let animal = makeAnimal()
print(animal.foo()) // ✅

animal の型は抽象的な A として扱われます。しかし、 A は制約上 Animal に適合しないといけないので、 animalfoo メソッドを持っていることは保証されます。そのため、 animal.foo() が実行できます。

また、 A は抽象的な型ですが、通常のジェネリクスと同じようにコンパイラによって 特殊化 できます。そのため、 実行時には ACat であるかのように扱われ、抽象化によるオーバーヘッドが発生しません。

ただし、 コンパイル時には A は型の上で明確に Cat と区別されます。 animal に格納されるインスタンスの実体は Cat ですが、 animalCat 型変数に代入することはできません。たとえ ACat であることをコンパイラが知っていても( 特殊化 できるということは知っているということです)、型エラーとしてコンパイルエラーにします。

let cat: Cat = animal // ⛔ animal の実体は Cat だけどコンパイルエラー

これは、 A の実体を隠蔽する上で重要です。もし、上記のコードを許すと何が起こるでしょうか。 makeAnimal 関数が Cat ではなく Dog を返すように変更されたとしましょう。すると、上記のコードは Dog インスタンスを Cat 型変数に代入しようとしていることになり、コンパイルエラーになってしまいます。

makeAnimal 関数の利用者目線では、これはとんでもないことです。この makeAnimal 関数が何らかのライブラリの API だったとしましょう。ライブラリをアップデートしたときに前述の変更が加えられていると、 makeAnimal 関数の型は変更されていないのに、それまでコンパイルが通っていたコードが急にコンパイルが通らなくなってしまうということです。たとえ ACat であることがわかっていても、最初から ACat を区別して扱っておくことでそのような事態を防ぐことができます。

makeIterator メソッドについても、 makeAnimal 関数と同じことが言えます。 Swift 5.1 時点では、 ArraymakeIterator メソッドは次のように IndexingIterator を返すことを露出してしまっています。

// 具象型戻り値
extension Array: Sequence {
    func makeIterator() -> IndexingIterator<[Element]> { ... }
}

リバースジェネリクスを使えば次のようにイテレータの型を隠蔽できます。しかも、 特殊化 されれば抽象化による実行時のオーバーヘッドはありません。

// 抽象型戻り値と具象型のパフォーマンス😄
extension Array: Sequence {
    func makeIterator() -> <I: IteratorProtocol> I
        where I.Element == Element { ... }
}

ジェネリクスが実行時のオーバーヘッドなく引数の型を抽象化できるように、 リバースジェネリクス を使えば実行時のオーバーヘッドなく戻り値の型を抽象化できるのです。

また、このとき makeIterator メソッドが返すイテレータの実体は IndexingIterator のままですが、それを IndexingIterator として受けることはできなくなります。

// makeIterator メソッドにリバースジェネリクスが使われたとき
var iterator: IndexingIterator<[Int]>
    = [2, 3, 5].makeIterator() // ⛔ コンパイルエラー

もし上記のコードが許されていると、将来的に ArraymakeIterator メソッドが、別のより効率的なイテレータを返すように変更されたときに、急に上記のコードがコンパイルエラーになってしまいます。初めから IIndexingIterator を区別しておくことで下記のようなコードを書くことを強制し、そのような問題の発生を防止することができます。

var iterator = [2, 3, 5].makeIterator() // ✅

Opaque Result Type

リバースジェネリクス を使えば抽象的な戻り値と具象型のパフォーマンスを両立できますが、必ずしもシンタックスがわかりやすいとは言えません。 makeAnimal 関数が何らかの Animal を返す場合、それを意味するコードは次の通りです。

// リバースジェネリクス
func makeAnimal() -> <A: Animal> A {
    Cat()
}

「何らかの Animal 」、つまり「ある Animal 」を返すわけです。英語で言えば「 some Animal 」です。次のように書ければよりわかりやすいはずです。

// Opaque Result Type
func makeAnimal() -> some Animal {
    Cat()
}

これが Opaque Result Type です。この “Result” は処理の「結果」、つまり、戻り値を意味しています。隠蔽された不透明( Opaque )な戻り値の型なので Opaque Result Type です。

Opaque Result Typeリバースジェネリクス を簡潔に書くためのシンタックスシュガーだと考えることができます。シンタックスシュガーなので、上記の二つのコード( リバースジェネリクス 版と Opaque Result Type 版の makeAnimal 関数)は全く同じことを意味します。

Opaque Result TypeSE-0244 で部分的に採択され、 Swift 5.1 でサポートされました。「部分的に」というのは、 Swift 5.1 時点では some Sequence<.Element == Int> のように associatedtype を指定したり、 Set<some Animal>[some Animal](some Animal)? のように型パラメータを埋めるときに some を使うことができないからです。これらの機能については SE-0244 や “Improving the UI of generics” の中で言及されており、 “first step” として Opaque Result Type の部分的な機能を導入すると述べられているため、将来的に導入される可能性が高いと考えられます。

Opaque Argument Type

Opaque Result Type を使えば リバースジェネリクス を簡潔に記述できました。同じことは通常のジェネリクスについても言えるはずです。ジェネリックな引数を some を使って簡潔に書けるようにしようというのが Opaque Argument Type です。

例として、通常のジェネリクスで書かれた次のような関数 useAnimal を考えます。

// ジェネリクス
func useAnimal<A: Animal>(_ animal: A) {
    print(animal.foo())
}

これを Opaque Argument Type で書いたコードが下記です。

// Opaque Argument Type
func useAnimal(_ animal: some Animal) {
    print(animal.foo())
}

Opaque Result Typeリバースジェネリクス の関係と同じように、 Opaque Argument Type はジェネリクスのシンタックスシュガーです。上記の二つの useAnimal 関数はどちらの書き方をしてもまったく同じ意味になります。

なお、 Opaque Argument Type は Swift 5.1 ではサポートされていません。 Opaque Result Type の完全なサポート同様、今後導入される可能性が高そうです。

ジェネリクスでしかできないこと

Opaque Result TypeOpaque Argument Type を合わせて Opaque Type と呼びます。 Opaque Type がジェネリクス( リバースジェネリクス を含みます)のシンタックスシュガーなら、 Opaque Type さえあればジェネリクスは不要なのでしょうか。そうではありません。ジェネリクスでしかできないことの例を見てみましょう。

たとえば、 Animal のつがいを引数に受け取る関数 useAnimalPair を考えてみます。

// ジェネリクス
func useAnimalPair<A: Animal>(_ pair: (A, A)) {
    ...
}

つがいなので、引数には同種の Animal を渡さなければなりません( CatDog ではいけません)。そのため、 pair の型は一つの型パラメータ A で書かれたタプル (A, A) となっています。

これを Opaque Type で書こうとするとどうなるでしょうか。

// Opaque Argument Type
func useAnimalPair( _ pair: (some Animal, some Animal)) { // これで良い?🤔
    ...
}

しかし、上記のコードは下記のコードと同じ意味になってしまいます。

// ジェネリクス
func useAnimalPair<A1: Animal, A2: Animal>(_ pair: (A1, A2)) { // これではダメ😵
    ...
}

some Animal を 2 回書いた場合、それらは異なる型を意味することになります。もし、最初の useAnimalPair 関数のように、同種の Animal を二つ引数にとりたい場合にはジェネリクスを使うしかありません。

このように、 Opaque Type ではなくジェネリクスでしかできないこともあります。 Opaque Type はジェネリクスでできることの一部を簡潔に書くための手段でしかありません。 このことからも、筆者はいずれ リバースジェネリクス は採択されるだろうと考えています。 Opaque Result Type が完全にサポートされても、 リバースジェネリクス でしかできないことが存在するからです。

Opaque Type と some

Opaque Type には some というキーワードを使いますが、筆者はこのキーワードの選び方が秀逸だと考えています。

下記のコードは、 先程 Opaque Type で書こうとした useAnimalPair 関数のものです。

func useAnimalPair( _ pair: (some Animal, some Animal)) {
    ...
}

このとき、二つの some Animal は別の型を意味しますが、字面の上ではまったく同じです。しかし、「ある( some ) Animal 」と「ある( some ) Animal 」が異なる Animal を示すのは言語的には自然です。 some の代わりに当初考えられていた opaque が選ばれていると、二つの opaque Animal が異なる型を表すというのはよりわかりづらかったでしょう。

また、次のような例からも some というキーワード選定の秀逸さが見て取れます。

func useAnimals(_ animals: [some Animal]) {
    ...
}

この関数の引数 animals は、 Homogeneous な(同種の値しか格納できない) Array です。「ある( some ) Animal 」の Array がある一種類の Animal のインスタンスしか格納できないことは、言語的に自然です。

SwiftUI と Opaque Type

Swift 5.1 と同時に SwiftUI がリリースされました。 SwiftUI で初めて Opaque Result Type に触れたという人も多いのではないかと思います。 Opaque Result Type のユースケースの例として、 SwiftUI の中で Opaque Result Type がどのように使われているのか、それがないと何が起こるかを見てみましょう。

SwiftUI を使うと宣言的に UI を記述できますが、その文脈では Function Builder に焦点が当たりがちです。しかし、メソッドチェーンを主体とした API もその一端を担っています。

たとえば、次のように padding メソッドと background メソッドを連ねることによって、下記の虹色の正方形を作ることができます。

Spacer().frame(width: 20, height: 20)
    .padding(20).background(Color.violet)
    .padding(20).background(Color.indigo)
    .padding(20).background(Color.blue)
    .padding(20).background(Color.green)
    .padding(20).background(Color.yellow)
    .padding(20).background(Color.orange)
    .padding(20).background(Color.red)

では、上記の式の型はどうなるでしょうか。一見 Spacerpadding をプロパティとして持っていれば、 Spacer().padding(...) の結果も同じ Spacer 型として扱えそうです。しかし、 padding を二つ連ねるとどうでしょうか。しかも、設定できるのは padding だけではありません。あらゆる設定値を複雑に組み合わせ入れ子状に設定できなければなりません。上のコードでも frame, padding, background の三つのメソッドを組み合わせて入れ子構造を作っています。

これを実現するには、たとえば Padding 型や Background 型を作って、それらの入れ子構造として表現することができます。そうすると、上記のメソッドチェーンは次のイニシャライザの入れ子と同じことにできます。

// ※ あくまで例であり、実際の SwiftUI とは異なります
Background(Padding(
    Background(Padding(
        Background(Padding(
            Background(Padding(
                Background(Padding(
                    Background(Padding(
                        Background(Padding(
                            Frame(
                                Spacer()
                            , width: 20, height: 20)
                        , 20), Color.violet)
                    , 20), Color.indigo)
                , 20), Color.blue)
            , 20), Color.green)
        , 20), Color.yellow)
    , 20), Color.orange)
, 20), Color.red)

この式の型であれば考えやすそうです。もし、 Background とそのイニシャライザが次にように実装されていれば、上記の式の型は Background です。

// ※ あくまで例であり、実際の SwiftUI とは異なります
struct Background: View {
    init(_ view: View, _ color: Color) { ... }
    ...
}

もし UIKit の UIView のように、 SwiftUI の View がビューの基底クラスになっているのであればこれで問題ありません。しかし、 SwiftUI の View はプロトコルです。そして、個々のビューは struct です。そもそも、前節で説明した通り、 View プロトコルは associatedtype Body を持っているため、これを抽象的な View 型として扱うことはできません。仮に扱うことができたとしても、 Existential Container のオーバーヘッドが発生してしまいます。 AnyView を使っても同様です。

では、 Background はどのように実装すれば良いでしょうか。そして、最初に挙げたメソッドチェーン(もしくは先のイニシャライザの入れ子呼び出し)の型はどうなるでしょうか。

抽象的な型を使わずに書くことを考えると、 Background の実装は次のようになります。

// ※ あくまで例であり、実際の SwiftUI とは異なります
struct Background<Content: View>: View {
    init(_ view: Content, _ color: Color) { ... }
    ...
}

Background に型パラメータを持たせて、対象となるビューの型を指定できるようにします。たとえば、 Background(Spacer(), Color.red) という式の型は Background<Spacer> 型になります。

PaddingFrame も同様です。

// ※ あくまで例であり、実際の SwiftUI とは異なります
struct Padding<Content: View>: View {
    init(_ view: Content, _ padding: CGFloat) { ... }
    ...
}

struct Frame<Content: View>: View {
    init(_ view: Content, witdh: CGFloat, height: CGFloat) { ... }
    ...
}

そうすると、最初のメソッドチェーン(もしくは先のイニシャライザの入れ子呼び出し)の型は次のようになります。

Background<Padding<Background<Padding<Background<Padding<Background<Padding<Background<Padding<Background<Padding<Background<Padding<Frame<Spacer>>>>>>>>>

とても長いですね。実際の SwiftUI ではもう少し複雑で、次のようになります。

ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<Spacer, _FrameLayout>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>

もしこのような虹色の正方形を表示するビュー RainbowSquare を実装しようとすると、 body プロパティの型としてそれが現れます。

struct RainbowSquare: View {
    var body: ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<Spacer, _FrameLayout>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>>, _PaddingLayout>, _BackgroundModifier<Color>> {
        Spacer().frame(width: 20, height: 20)
            .padding(20).background(Color.violet)
            .padding(20).background(Color.indigo)
            .padding(20).background(Color.blue)
            .padding(20).background(Color.green)
            .padding(20).background(Color.yellow)
            .padding(20).background(Color.orange)
            .padding(20).background(Color.red)
    }
}

とても書けたものではありません。しかも、虹を 7 色から 8 色に変更しようと思った場合(虹を何色で表すかは地域によって異なります)、 body プロパティの中身だけでなく型まで変更しなければなりません。少しコードを変更しただけで body の型が変わってしまうのでは、中身の実装に集中することができません。

Opaque Result Type を使えば、この長ったらしいビューの型を some View として簡潔に記述できます。 body の中身を変更しても、 some View の部分を変更する必要はありません。

struct RainbowSquare: View {
    var body: some View {
        Spacer().frame(width: 20, height: 20)
            .padding(20).background(Color.violet)
            .padding(20).background(Color.indigo)
            .padding(20).background(Color.blue)
            .padding(20).background(Color.green)
            .padding(20).background(Color.yellow)
            .padding(20).background(Color.orange)
            .padding(20).background(Color.red)
    }
}

このように、 Opaque Result Type を使って body プロパティの実際の型を隠蔽したことで何か不都合が生じるでしょうか。 API の利用者にとって、この body プロパティの実際の型が何であるかは重要ではありません。 body の型が View プロトコルに適合していることがわかれば十分です。つまり、「ある( some ) View 」で良いわけです。一般的に、 body プロパティの型を some View に隠蔽して困ることはありません。 some View 型として表現することは、抽象度として適切な選択だと言えるでしょう。

そのような理由で、 Opaque Result Type は SwiftUI で欠かせない役割を果たしています。

Opaque Type と Existential Type

ここで Opaque TypeExistential Type の関係を整理しておきたいと思います。どちらも抽象化のために使用されます。

例として、 useAnimal 関数の引数の型の抽象化を考えます。 Opaque Type の場合、次のようになります。

// Opaque Type
func useAnimal(_ animal: some Animal) {
    print(animal.foo())
}

一方で、 Existential Type の場合は下記のようになります。このとき、 Existential Type の方が引数 animal の型を簡潔に記述できます。

// Existential Type
func useAnimal(_ animal: Animal) { // Opaque Type より簡潔
    print(animal.foo())
}

しかし、上記の二つのコードの内、望ましいのは Opaque Type による記述です。 Opaque Type はジェネリクスのシンタックスシュガーであり、実行時のオーバーヘッドがありません。にも関わらず、今のシンタックスでは Existential Type の方が簡潔に書けてしまっています。

ジェネリクスと Existential Type ではシンタックスの見た目が大きく異なったためあまり気になりませんでしたが、 Opaque Type を導入するとシンタックスの見た目が近くなったこともあり、この逆転が気になります。

そこで、 Opaque Typesome というキーワードが必要なように、 Existential Type にも any というキーワードを付与することが “Improving the UI of generics” の中で提案されていますany が導入されると、たとえば、上記の Existential Type を使った useAnimal 関数は次のようになります。

// Existential Type
func useAnimal(_ animal: any Animal) { // any が必要
    print(animal.foo())
}

このシンタックスが採用されれば、 Opaque TypeExistential Type の記述が、 some Animal vs Animal だったのが some Animal vs any Animal となり、シンタックスの上で対等になります。

さらに、 any の導入にはもう一つ大きな意味があります。これまで見てきた通り、 プロトコルには、「型として」・「制約として」の二通りの異なる使い方があります。 any が導入されれば、型としてのプロトコル( Existential Type )には any が付与されることとなり、プロトコルがどちらの使い方をされているのかコード上で一目瞭然となります。

func useAnimal(_ animal: any Animal) { // any があるので「型として」
    print(animal.foo())
}

func useAnimal<A: Animal>(_ animal: A) { // any がないので「制約として」
    print(animal.foo())
}

また、筆者はこの someany というキーワードは素晴らしい選択だと考えています。 some というキーワードの秀逸さについては先に述べましたが、 any と対比することでその素晴らしさがより際立ちます。

先程と同じように、 some AnimalArray を受け取る useAnimals 関数を考えてみます。

// Opaque Type
func useAnimals(_ animals: [some Animal]) {
    ...
}

このとき、引数 animals は「ある( some ) Animal 」の Array です。「ある Animal 」の Array が特定の一種類の Animal しか格納できない( Homogeneous である)ことは言語的に自然です。

// Opaque Type
useAnimals([Cat(), Dog()]) // ⛔
useAnimals([Cat(), Cat()]) // ✅
useAnimals([Dog(), Dog()]) // ✅

同じように、 any AnimalArray を受け取る useAnimals 関数を考えます。

// Existential Type
func useAnimals(_ animals: [any Animal]) {
    ...
}

このとき、引数 animals は「任意の( any ) Animal 」の Array です。「任意の Animal 」の ArrayCatDog を混在させることのできる( Heterogeneous である)ことは言語的に自然です。

// Existential Type
useAnimals([Cat(), Dog()]) // ✅

つまり、 Opaque TypeExistential Type という型システム上の概念が、 someany というキーワードによって言語的にも対応しているわけです。シンタックスがセマンティクスを自然に表しているという意味で、すばらしいキーワード選定だと言えるのではないでしょうか。

まとめ

本節では リバースジェネリクスOpaque TypeExistential Type など多くの項目を扱いましたが、それらの関係を表にまとめると次のようになります。

  引数 戻り値
ジェネリクス
Type-level abstraction
制約としてのプロトコル
<A: Animal>(A) -> Void リバースジェネリクス
() -> <A: Animal> A
未サポート
Opaque Type
Type-level abstraction
ジェネリクスのシュガー
Opaque Argument Type
(some Animal) -> Void
未サポート
Opaque Result Type
() -> some Animal
一部サポート
Existential Type
Value-level abstraction
型としてのプロトコル
(any Animal) -> Void
一部サポート
() -> any Animal
一部サポート

上二段の ジェネリクスOpaque Type は、コンパイル時に静的に抽象化を扱います。コード上で抽象化された型がコンパイル時に 特殊化 によって展開され、実行時のパフォーマンスに影響のない抽象化が可能となります。 Core Team の Joe Groff さんは “Improving the UI of generics” の中で、これを Type-level abstraction と呼んでいます。

一方、最下段の Existential Type は、実行時に動的に抽象化を扱います。このときの主役は値です。値はプロトコルに適合していることだけが求められ、その型は重視されません。値の挙動は Existential Container を用いて動的に解決されます。 Joe Groff さんはこれを Value-level abstraction と呼んでいます。

Swift は 値型 中心の言語であるにも関わらず、 値型 前提で抽象化を考えたときに、実行時のオーバーヘッドなく戻り値の型を抽象化する方法がありませんでした(戻り値 × Type-level abstraction)。 リバースジェネリクス とそのシンタックスシュガーである Opaque Result Type によって、パフォーマンスを損ねることなく戻り値の型を抽象化できるようになりました。

ただし、いつでも Type-level abstraction で良いというわけではなく、状況に応じて Value-level abstraction も必要となります。そういう意味で、 Existential Type も Swift には欠かせない存在です。その Existential Typeany というキーワードを付与することが提案されており、それによって Existential TypeOpaque Type をシンタックス上、対等にすることができます、また、 any が付与されているかどうかで、プロトコルが型として用いられているのか、制約として用いられているのかがシンタックス上わかりやすくなります。

“Improving the UI of generics” の中でこれらの関係性が示されて、 Swift に必要な道具が明確になりました。しかし、上記の表の通り、 Swift 5.1 時点ではまだまだサポートされていない道具が多くあります。 Opaque Result Type は、現時点では [some Animal]some Sequence<.Element == Int> などができません。 Existential Type についても、 Generalized Existentialany Sequence<.Element == Int>any View など)が欠けたままです。 Opaque Argument TypeReverse Generics については部分的にすらサポートされていません。それらの必要性は、本章を通して、 Swift が 値型 中心の言語であることから必然的に導かれました。すべてが揃うことによって、 Swift は 値型 中心の言語として、より完成に近づけることでしょう。