いろんな型消去
初心者です。初心者なので型システム入門を何周してもわかりません。無限に周回と挫折を繰り返しています。誰か型システム入門の攻略チャートを組んでください……
なんで型消去を選んだ
さて、型システム入門のなかで 型消去
という語が登場します。皆さんは型消去と聞いて何を思い浮かべるでしょうか。私は普段Swiftを書くのでSwiftの型消去テクニックを真っ先に思い出します。これは型システム入門で紹介される型消去とは異なります多分。どうやら「型消去」という3文字自体はあくまで型を消去するという意味しかなく、どんな形であれ型を消す作業をしていればそれは型消去となってしまうようです。さっぱりわからん。
Wikipediaの型消去のページにはC++の例とJavaの例が載っています。
型消去 - Wikipedia
Wikipediaで説明されているJavaの型消去は、恐らく型システム入門の言う 型消去意味論
の一環として行われていることだとは思うのですが、意味論として型消去について語っている資料が英語論文くらいしかないので読んでないよくわからん。
本記事では各言語における型消去の操作について整理します。そうすれば型消去意味論の意味を理解できるかもしれないから。
Swiftの型消去
Swiftにはprotocolというものがあり、何らかの機能に必要となるプロパティやメソッドを定義する。標準ライブラリでは同値比較用のprotocolやシーケンス用のprotocolなどを提供している。
protocolにはassociated typeという仕組みがあり、protocolの定義段階では仮の型名で保留にしておき、クラスがprotocolを準拠する時に具体的な型を指定できる。
protocol Container { // 具体的に何の型が取れるのかはここでは定めない associatedtype Element func take() -> Element }
associated typeの問題点は、associated typeを含むprotocol型の変数を宣言できないことにある。
class MyBox<T> { private let item: T init(_ item: T) { self.item = item } } extension MyBox: Container { typealias Element = T func take() -> T { return item } } // `MyBox` はただのクラスなのでOK let a: MyBox<Int> = MyBox<Int>(0) // protocol 'Container' can only be used as a generic constraint // because it has Self or associated type requirements // というエラーになる let b: Container = MyBox<Int>(0)
どうしても宣言したい時は型消去をする。Any(protocol名)
と名付けて使うのが慣例。標準ライブラリでも AnySequence
などが提供されている。
// Container型の変数を定義することはできないが、総称型に制約をつけることはできる。 struct AnyContainer<T> where T: Container { private let _container: T init(_ container: T) { _container = container } } let c: AnyContainer<MyBox<Int>> = AnyContainer(MyBox<Int>(0))
以上がSwiftの型消去テクニック。Swiftを書く人間がやるテクニックであって、コンパイラがどうとか実行時がどうとかという話ではない。
C++の型消去
上記のSwiftの型消去は多分C++の型消去技法に由来するのだと思う(C++を意識してないってこたぁないっしょ)。
Wikipediaでも説明があるが、C++ではテンプレートを駆使してAny型を作る。型安全を無視すればテンプレート以外の方法で型消去することも可能、らしい。
型の力を抜いて学ぶC++ - Google スライド
「C++ 型消去」でGoogle検索をして出てくるのはこのテクニックの話。次に説明するJavaだと総称型関連の話がわんさか出てくるのだが、C++の総称型は型消去と異なる手法(こちらは全展開とか異種変換とか呼ばれるっぽい)を採用しているためかテクニックの話ばかり出てくる、ように見える。
Javaの型消去
本記事で取り上げる型消去の中では2番目に型消去意味論に近そう?に見えるやつ(個人の感想です)。
Javaに総称型が導入されたのは1.5のとき。Java仮想マシンの互換性を重視したので、プログラマが型変数を書いてもクラスファイルには型変数の情報を残さない、つまり型消去することにした。実際、コンパイル&デコンパイルをすると型変数が復元されない。
元のソースコードが以下のようだとすると、
class GenericClass<T>{ private T value; public GenericClass(T value){ this.value = value; } public T getValue(){ return value; } }
javap
はこんな感じ。これは1.5で試した場合であって、今は型変数に関する情報をクラスファイルに書き残しており復元可能になっている。そうでないとデバッガが困るので。
class GenericClass extends java.lang.Object{ public GenericClass(java.lang.Object); public java.lang.Object getValue(); }
Object
型に置き換えるこのやり方は同種変換と呼ばれる。どんなものが型変数に代入されようが、Object
型なので何とかなる。ちなみにC++の異種変換は、ソースコード中で代入される型(int
とかstring
とか)へ置き換えたものを個別に用意するというもの。用意する分だけサイズが増えるが実行効率はこっちが有利らしい。
なので、Javaの場合は「同種変換という手法をとる過程で型消去して Object
に置き換えた」となるのではなかろうか。
TypeScriptの型消去
- Javaと同様に、総称型は型消去によって実現しているらしい。
- コンパイルしてJavaScriptコードを生成するときに型アノテーションを消すことも型消去と呼ぶらしい。
Google検索でざっと眺めたところでは、普通は1の用法で、稀に2の用法で使っていることがあるように見える。まあわざわざ2の話をすることはないでしょうね。だが高級言語→低級言語へコンパイルする過程で型をひたすら消去した、と見立てれば型消去意味論に最も近い気がする(個人の感想です)。
FAQ · microsoft/TypeScript Wiki · GitHub
まとめ
この辺読むと楽しいよ
理解の足りてない本記事よりこれらを当たるべき
- https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-JA16-Generics.pdf
- https://ipsj.ixsq.nii.ac.jp/ej/?action=repository_uri&item_id=65070&file_id=1&file_no=1
- Chapter 4. The class File Format
- Type Erasure (The Java™ Tutorials > Learning the Java Language > Generics (Updated))
- erasure
- ジェネリック - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
- https://www.fos.kuis.kyoto-u.ac.jp/~igarashi/papers/slides/JSSST06-tutorial.pdf
来月の技術書典にはもうちょっとしっかりまとめるつもり、論文も1本くらいは読むつもり