I Want a Type-alee-uss-ess-ess for Christmas Russ Bishop January 05, 2015
- 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
}
は?
クラス、構造体、列挙型と違い、protocolは総称型パラメータをサポートしていない。代わりに抽象型メンバーをサポートしている; Swiftの用語ではAssociated Typeと呼ぶ。どちらのシステムでも多くのことを達成できるが、associated typeにはいくつか利点がある(現在のところ欠点もいくつかある)。
protocolのassociated typeは「これがどんな正確な型かは知らない;何か具体的なクラス/構造体/列挙型が詳細を埋める」ということを示す。
「すばらしい!」と言ってあなたは泣く、「では何故型パラメータと違うのか?」いい質問である。型パラメータは全員が関係する型を知って繰り返しそれを指定することを強制する(型パラメータと共に作成すると型パラメータの数が増大する可能性がある)。それらはpublic interfaceの一部である。具体的なもの(クラス/構造体/列挙型)を使うコードはどの型を使うのかを決定する。
対照的にassociated typeは実装の詳細の一部である。クラスがinternal ivarsを隠せるように、associated typeも隠すことができる。抽象型メンバーは具体的なもの(クラス/構造体/列挙型)があとで提供するためのものである。クラス/構造体をインスタンス化する時ではなくprotocolを採用するときに実際の型を選ぶ。異なる手の集合において、どの型を選ぶかに対する管理権を残す。
Scala作者のMary Oderskyはこのインタビューで例を議論している。
Swift的には、基底クラス/protocolでメソッド eat(f:Food)
を持つ Animal
があるときにassociated typeがないと、クラス Cow
は Food
が Grass
のみであると指定する手段がない。確かにメソッドのオーバーロードで行うことはできない - 共変パラメータ型(パラメータをサブクラスに指定する)はほとんどの言語でサポートされておらず、基底クラスへのキャストが予期しない値を与えるので安全ではない。
もし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は「 Grass
は Food
と同じではない」と不平を言うだろう。ここでもこのメソッドは 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
クラスの実装の詳細だが、その情報はコンパイル時にはまだ知られている。
動物のことはもう十分; Swiftの CollectionType
(訳注: 現 Collection
)を見てみよう。
注意: 実装の詳細として、いくつかのSwift protocolには入れ子になったprotocolがあり、先頭にアンダースコアがついている;
CollectionType
→_CollectionType
やSequenceType → _Sequence_Type → _SequenceType
。簡潔にするために、これらのprotocolについて話すときにはヒエラルキーを平坦にする。したがって、CollectionType
がassociated typeItemType
、IndexType
、GeneratorType
を持っていると言っても、CollectionType
protocl自身にそれらの型を見つけることはできない。
(訳注: 現在はこれらの型はなくなっているか名前が変わっている)
明らかに要素の型 T
が必要だが、 subscript(index:S) -> T { get }
と func generate() -> G<T>
を扱うためにはindexとgenerator/enumeratorの型も必要である。型パラメータを使用していた場合、 Collection
protocol が動作する唯一の方法は仮想上の CollectionOf<T,S,G>
で T
、 S
、 G
を指定することである。
他の言語はどうか?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.
抽象型メンバーを持つ主要な"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 EdibleFood
が Grass
であるという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>:Generator
、 SequenceOf<T>:Sequence
、 SinkOf<T>:Sink
があるのか疑問を思ったことがあったなら…今あなたは知っている。
上で述べたバグは、もし Animal
が typealias EdibleFood:Food
を指定すると、 struct SpecificAnimal<F:Food,S>:Animal
として定義してもこの構造体がコンパイルできないということである。構造体に対する制約が明確にそれを示していても、Swiftは F
が Food
ではないと不平を言うだろう。このバグはrdar://19371678として提出されている。
(訳注: https://github.com/apple/swift/blob/master/test/Generics/same_type_constraints.swift)
見てきたように、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