Skip to content

Instantly share code, notes, and snippets.

@S-Shimotori
Last active August 4, 2017 03:47
Show Gist options
  • Save S-Shimotori/e389859a692260e5bbcfe0de8110cb3d to your computer and use it in GitHub Desktop.
Save S-Shimotori/e389859a692260e5bbcfe0de8110cb3d to your computer and use it in GitHub Desktop.

Swift: Associated Types

I Want a Type-alee-uss-ess-ess for Christmas Russ Bishop January 05, 2015

Associated Types Series

  • Swift Associated Types
  • Swift Associated Types, cont.
  • Swift: Why Associated Types?

Sometimes I think type theory is deliberately obtuse and all those functional programming hipster kids are just bull-shitting as if they understood one word of it. Really? You have a 5,000 word blog post about insert random type theory concept? And it doesn't explain a) why anyone should care and b) what problems are solved by this wonderful concept's introduction? I want to tie you up in a sack, throw the sack in a river, and hurl the river into space.

Where was I? Oh yeah, Associated Types.

When I first saw Swift's implementation of generics, the use of Associated Types jumped out at me as strange.

In this post I'm going to work through the type theory and some practical considerations, mostly as an attempt to cement the concepts in my own mind (if I make any mistakes, please let me know!)

総称型

Swiftで型を抽象化する(一般的なものを作成する)なら、クラスに次の構文を用いる:

class Wat<T> { ... }

同様にして、総称型構造体:

struct WatWat<T> { ... }

総称型列挙型:

enum GoodDaySir<T> { ... }

しかし抽象的なprotocolが欲しいと:

protocol WellINever {
    typealias T
}

は?

Basics

クラス、構造体、列挙型と違い、protocolは総称型パラメータをサポートしていない。代わりに抽象型メンバーをサポートしている; Swiftの用語ではAssociated Typeと呼ぶ。どちらのシステムでも多くのことを達成できるが、associated typeにはいくつか利点がある(現在のところ欠点もいくつかある)。

protocolのassociated typeは「これがどんな正確な型かは知らない;何か具体的なクラス/構造体/列挙型が詳細を埋める」ということを示す。

「すばらしい!」と言ってあなたは泣く、「では何故型パラメータと違うのか?」いい質問である。型パラメータは全員が関係する型を知って繰り返しそれを指定することを強制する(型パラメータと共に作成すると型パラメータの数が増大する可能性がある)。それらはpublic interfaceの一部である。具体的なもの(クラス/構造体/列挙型)を使うコードはどの型を使うのかを決定する。

対照的にassociated typeは実装の詳細の一部である。クラスがinternal ivarsを隠せるように、associated typeも隠すことができる。抽象型メンバーは具体的なもの(クラス/構造体/列挙型)があとで提供するためのものである。クラス/構造体をインスタンス化する時ではなくprotocolを採用するときに実際の型を選ぶ。異なる手の集合において、どの型を選ぶかに対する管理権を残す。

Usefulness

Scala作者のMary Oderskyはこのインタビューで例を議論している

Swift的には、基底クラス/protocolでメソッド eat(f:Food) を持つ Animalがあるときにassociated typeがないと、クラス CowFoodGrass のみであると指定する手段がない。確かにメソッドのオーバーロードで行うことはできない - 共変パラメータ型(パラメータをサブクラスに指定する)はほとんどの言語でサポートされておらず、基底クラスへのキャストが予期しない値を与えるので安全ではない。

もしSwiftのprotocolが型パラメータをサポートしていたらこのようになるだろう:

protocol Food { }
class Grass : Food { }

protocol Animal<F:Food> {
    func eat(f:F)
}

class Cow : Animal<Grass> {
    func eat(f:Grass) { ... }
}

すばらしい。ただの食べ物以上のことを追跡しなければならない時は何が起きるか?

protocol Animal<F:Food, S:Supplement> {
    func eat(f:F)
    func supplement(s:S)
}

class Cow : Animal<Grass, Salt> {
    func eat(f:Grass) { ... }
    func supplement(s:Salt) { ... }
}

型パラメータの数が増えるのは残念だが、問題はそれだけではない。実装の詳細が全面的に漏れているので、型パラメータを繰り返し指定する必要がある。 var c = Cow() の型は実際に Cow<Grass,Salt> である。 doCowThings 関数は func doCowThings(c:Cow<Grass,Salt>) になる。草を食べる全ての動物と共に処理を行いたいのに? Supplement 型パラメータについて気にしないことを表現する方法もない。

特定の品種を作るために Cow から派生させるときにはこのクラス定義は馬鹿馬鹿しい: class Holstein<Food:Grass, Supplement:Salt> : Cow<Grass,Salt>

さらに悪いことに、食べ物を買って動物に与える関数はどうか: func buyFoodAndFeed<T,F where T:Animal<Food,Supplement>>(a:T, s:Store<F>) 。本当に醜くて冗長であるだけでなく、 F の型を Food に結びつける方法がない。関数定義を書き換えると func buyFoodAndFeed<F:Food,S:Supplement>(a:Animal<Food,Supplement>, s:Store<Food>) で対処できるが、これは動かない - Swiftは「 GrassFood と同じではない」と不平を言うだろう。ここでもこのメソッドは Supplement について気にしないのに対処しなければならないということに注意する。

次に、associated typeがどのように役立つか見てみよう:

protocol Animal {
    typealias EdibleFood
    typealias SupplementKind
    func eat(f:EdibleFood)
    func supplement(s:SupplementKind)
}
class Cow : Animal {
    func eat(f: Grass) { ... }
    func supplement(s: Salt) { ... }
}
class Holstein : Cow { ... }

func buyFoodAndFeed<T:Animal, S:Store 
    where T.EdibleFood == S.FoodType>(a:T, s:S)
{ ... }

型シグネチャははるかに明確である。Swiftは Cow のメソッドシグネチャを見るだけでassociated typeを推測する。 buyFoodAndFeed メソッドは、動物が食べる食べ物の種類を店が売っているという要件を明確に表現できる。 Cow が特定の種類の食べ物を必要とする事実は Cow クラスの実装の詳細だが、その情報はコンパイル時にはまだ知られている。

Getting Real

動物のことはもう十分; Swiftの CollectionType (訳注: 現 Collection )を見てみよう。

注意: 実装の詳細として、いくつかのSwift protocolには入れ子になったprotocolがあり、先頭にアンダースコアがついている; CollectionType_CollectionTypeSequenceType → _Sequence_Type → _SequenceType 。簡潔にするために、これらのprotocolについて話すときにはヒエラルキーを平坦にする。したがって、 CollectionType がassociated type ItemTypeIndexTypeGeneratorType を持っていると言っても、 CollectionType protocl自身にそれらの型を見つけることはできない。

(訳注: 現在はこれらの型はなくなっているか名前が変わっている)

明らかに要素の型 T が必要だが、 subscript(index:S) -> T { get }func generate() -> G<T> を扱うためにはindexとgenerator/enumeratorの型も必要である。型パラメータを使用していた場合、 Collection protocol が動作する唯一の方法は仮想上の CollectionOf<T,S,G>TSG を指定することである。

他の言語はどうか?C#には抽象型メンバーはない。indexを一方向にのみ動かせるかどうかやランダムアクセスをサポートするかどうかなどについて型システムが何も言わないという自由なindex作成以外をサポートしないことで、まずこれを扱う。数値indexはただの整数であり、型システムはそれ以外のことを示さない。

次に、generator IEnumerable<T>IEnumerator<T> を吐き出す。相違点は最初非常に微妙だが、C#での解決策はinterface(protocol)を使ってgeneratorを間接的に抽象化することで、generatorの型を IEnumerable<T> のパラメータとして指定する必要がない。

Swiftは伝統的にコンパイルされた(VMでもJITでもない)システムプログラミング言語を目指しているので、動的な振る舞いの類を要求することはパフォーマンスにとって良いアイデアではない。インライン化や割り当てるメモリの量を知るといった派手なことをできるように、コンパイラはindexやgeneratorの型を知りたい。そのようなことができる唯一の方法は、コンパイル時にソーセージグラインダーでこれらの全総称型を動かすことである。これを実行時に延期するように強いると、that means indirection, boxing, and other such tricks which are nice when you need them but aren't free.

The Ugly Truth

抽象型メンバーを持つ主要な"gotcha"がある: 役に立たないので、実際にはSwiftはこれらを変数やパラメータ型として宣言することはできない。 protocolをassociated typeに使える唯一の場所は総称型制約である。 There's a major "gotcha" with abstract type members: Swift won't actually let you declare them as variable or parameter types because that would be useless. The only place you can use a protocol with associated types is as a generic constraint.

先の Animal の例の場合、 Animal はabstract EdibleFood を取って我々はそれが何かを知らないので、 Animal().eat を呼ぶのは安全ではない。

理論上では,関数に対する総称型制約が動物は店売りのものを食べると強いていたので次のコードは機能するはずだが、実際にはテスト時に EXC_BAD_ACCESS でクラッシュしてしまったので、このシナリオは煮詰まっていない。

func buyFoodAndFeed<T:Animal,S:StoreType 
    where T.EdibleFood == S.FoodType>(a:T, s:S) {
    a.eat(s.buyFood()) //crash!
}

これらの種のprotocolをパラメータや変数の型として使えないことが真の不利な点である。これは非常に多くの不要なループをジャンプすることを要求する。これは将来的にSwiftが改善できる(多分改善する)領域である。私はこのように変数や型を宣言する能力が欲しい:

typealias GrassEatingAnimal = 
    protocol<A:Animal where A.EdibleFood == Grass>

var x:GrassEatingAnimal = ...

注意: ここでの typealias の使用は実際にタイプエイリアスを作成するもので、protocolにassociated typeを作るものではない。紛らわしいことは承知している。

この構文により、 動物の associated type EdibleFoodGrassであるというAnimalの何らかの種を保持する変数を宣言することができる.これはprotocol内のassociated type自体が制約を受けている場合は自動的にこれを許容するのが便利かもしれないが、安全ではない状態に陥るのでより慎重な考えが必要になる。もしprotocolの定義内のassociated typeに制約を与えると、コンパイラはどのメソッド総称型制約に対しても充足することができない(以下を参照)。

現在はassociated typeを型パラメータに交換し"消去"する構造体ラッパーを作成することで、この問題を回避できる。警告: 醜い。

struct SpecificAnimal<F,S> : Animal {
    let _eat:(f:F)->()
    let _supplement:(s:S)->()

    init<A:Animal where A.EdibleFood == F, A.SupplementKind == S>(var _ selfie:A) {
        _eat = { selfie.eat($0) }
        _supplement = { selfie.supplement($0) }
    }

    func eat(f:F) {
        _eat(f:f)
    }
    func supplement(s:S) {
        _supplement(s:s)
    }
}

なぜSwiftの標準ライブラリに GeneratorOf<T>:GeneratorSequenceOf<T>:SequenceSinkOf<T>:Sink があるのか疑問を思ったことがあったなら…今あなたは知っている。

上で述べたバグは、もし Animaltypealias EdibleFood:Food を指定すると、 struct SpecificAnimal<F:Food,S>:Animal として定義してもこの構造体がコンパイルできないということである。構造体に対する制約が明確にそれを示していても、Swiftは FFood ではないと不平を言うだろう。このバグはrdar://19371678として提出されている。

(訳注: https://github.com/apple/swift/blob/master/test/Generics/same_type_constraints.swift)

Wrap It Up

見てきたように、associated typeにより、一連の型パラメータによる型定義の汚染なしでコンパイル時にprotocolの実装側が複数の具体的な型を提供できる。これは問題に対する興味深い解決策であり、総称型パラメータ(パラメータ化)とは異なる種の抽象(抽象メンバー)である。

Scalaのようなアプローチを採用して型パラメータとassociated type両方をクラス/構造体/列挙型/protocolでサポートすればより良い長期的なアプローチになると考える。これに関して多くの思考を与えていないので、いくつか主要な悪夢が潜んでいる可能性はある。これは、新しい言語に関して非常にエキサイティングなことの1つである - 時間の経過とともに進化し改善していく。

Now go forth and dazzle your colleagues and coworkers with fancy terms like Abstract Type Member. Then you too can lord it over them and render comprehension impossible.

Just stay away from sacks.

And rivers.

Not space. Space is awesome.

Russ Bishop This blog represents my own personal opinion and is not endorsed by my employer.

Subscribe to this Blog

Search

Share
Read Next: Swift: Strange tales of initialization

© 2017 Russ Bishop

Swift: Why Associated Types?

It's rabbit holes all the way down Russ Bishop May 03, 2016

Associated Types Series

  • Swift Associated Types
  • Swift Associated Types, cont.
  • Swift: Why Associated Types?

In my last article I gave an incorrect explanation for why Swift has associated types. It was half-correct in that specific knowledge of the types gives the compiler the ability to optimize but that's really an orthogonal issue and a result of the way the Swift Standard Library models various concepts using associated types. My apologies! I'm going to attempt to make it up to you with a better and clearer explanation.

You can express almost anything in terms of either associated types or type parameters. You can make the types statically or dynamically known in either system. So what are the differences?

Why Associated Types

associated typeを使うことで、独立した型、それらに対する制約、その間の関係について同時に何らかのものを示す単一のコンセプトを綺麗に表現することができる。以降protocolと気にしている型メンバーだけを参照することができる。

型パラメータで同じことをすると冗長的になる。モデル化したいコンセプトの各様相は新しい型パラメータ(おそらくいくつかの制約がつく)になる。これらの型パラメータと制約は、型パラメータのうちの1つだけを気にする場合(あるいは全く気にしない場合)でも型パラメータが使用される全ての箇所で繰り返す必要がある。

型パラメータも本質的には脆弱である。 Any addition of a type parameter to a protocol would immediately break all existing uses of that protocol whether the user of that protocol cares about it or not. (コンパイラが自動的に波及させない限り、制約を扱うと同じ問題を引き起こす。)対照的に、新しいassociated typeを追加してもprotocolの既存の使用者には何の影響もない。 型パラメータも本質的に脆弱です。 タイプパラメータをプロトコルに追加すると、そのプロトコルのユーザが気にするかどうかにかかわらず、そのプロトコルの既存の使用をすべて直ちに破棄することになります。 対照的に、関連する新しいタイプを追加しても、プロトコルの既存のユーザーには何の影響もありません。

パラメータ化

パラメータ化されたprotocolは曖昧な問題も発生させる: 型が SomeProtocol<String>SomeProtocol<UITableViewCell> を採用したら何が起きるか?

C#では全protocolのうち1つを除いたものを基本的には明示的に、基本的に隠すことを要求することで解決している。代替の実装にアクセスするためにはオブジェクトをprotocolにキャストしなければならない。Swiftでは、 func remove() implements SomeProtocol<UITableViewCell> { ... }SomeProtocol<String> 実装がデフォルトの可視版である一方でオブジェクトを SomeProtocol<UITableViewCell> にキャストしたときのみ呼べるようになるだろう。型パラメータは関数シグネチャには現れないことに注意し、 remove() の誤まったバージョンが呼ばれる時の混乱を想像してみて欲しい。

何かが起きた時のもう1つの選択肢は医者のアドバイスをとることである: "stop doing that"。パラメータ化された同じプロトコルをパラメータを変えて複数回採用することを禁止する。私はこれがSwiftのとる道だと思う。

結論は…?

Swiftがパラメータ化されたprotocolを持ちassociated typeのサポートがない場合でも、associated typeは依然としてそこに存在する; 代わりに型パラメータで表現される。しまいには、sequenceはelement型、generator型、index型、subsequence型が必要である。これらの型が型メンバーや型パラメータとして指定されるかどうかは、sequenceの完全なアイデアを形作るためにこれらが存在しなければならないという事実を変えるものではない。

なぜassociated typeなのかという質問の哲学的な答えは、Swiftのコアチームはそれがコンセプトをモデル化するのにより良い方法だと考えているからである。彼らは複数の準拠の問題を持たず、コンセプトの詳細を綺麗にカプセル化し、脆弱ではない。

唯一の欠点は、最後の投稿で議論した存在感を持つことができないことである。制限が解除されたとして、何が気に入らないのか?

Russ Bishop This blog represents my own personal opinion and is not endorsed by my employer.

Subscribe to this Blog

Search

Share
Read Next: Swift Associated Types, cont.

© 2017 Russ Bishop

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment