Type Inference vs. Static/Dynamic Typing

Herb Sutter "Type Inference vs. Static/Dynamic Typing"の適当な訳です。

var/autoの使い方に関するヒントになるかと思ったら、var/autoと動的型付けを混同しちゃだめだぜ、というお話。本邦のブログ界隈ではこの手の混同は見なかった気がするけど。

…なんでそこからvariant/anyの話になるのかいまいち分からないんですが。


ちょうどJeff Atoodが"a nice piece on why type inference is convenient"という記事を書いている。その中で彼はC#のサンプルを使って次のように書いている:

こんなコードを

StringBuilder sb = new StringBuilder(256);
UTF8Encoding e = new UTF8Encoding();
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();

こんな風に

var sb = new StringBuilder(256);
var e = new UTF8Encoding();
var md5 = new MD5CryptoServiceProvider();

書き直すことができるなんて、まったく身震いしちゃったよ。
これは動的型付けじゃない、本質的にはね。C#はいまだにとても強力な静的型付け言語だ。これのほとんどはコンパイラのトリックで、「可能な限り静的型付けで、必要なら動的型付けで」という世界への小さな一歩だ。

次のような事柄の間に明確な区別をつけることには価値がある:

  • あらゆる言語であなたができるような型推論
  • 静的型付け vs. 動的型付け:これは完璧に直交するものだが、型推論と混同されすぎている
  • 強い型付け vs. 弱い型付け:これはほとんど直交する(例えば、Cはすべての変数が静的に知られた実際の型を持っているから静的型付けであるが、キャストのために弱い型付け言語でもある)

上述の記事で、Jeffは型推論と動的型付けを明確に分けている。彼は型推論が動的型付けへの小さな一歩であるという自説を進めている。この主張は、文体的には原則的に真であるものの、残念ながら読者の一部を誤読させる可能性がある。つまり、型推論は動的性質と何らかの関係がある、と。実際にはそうではないのに。

型推論

上記のC#や次の標準C++C++0x, これは以下で述べる)など多くの言語で型推論機構が提供されている。C++0xはautoキーワードを再利用することでこれを実現している。例えば、map>型のオブジェクトmがあって、そのイテレータが欲しいとする:

map<int,list<string>>::iterator i = m.begin(); 	// 今日のC++では型が必要(C++0xでも許される)
auto i = m.begin(); 	// C++0xでは型は推論される

コンパイラよ、お前はその型が何だか既に知っているくせに、なぜまた俺に繰り返させようとするんだ?」そう言ったことが何度もあるでしょう?IDEでさえ、式の上にカーソルをのせたらその型を教えてくれるし。

C++0xではもうそんな事を言わなくても済む。これはかなり便利だ。型を自分の手で書きたくない、あるいは書くことができないようになってきているのだから、このことは日増しに重要になってきている。というのも次のような型を扱うことが増えてきているからだ:

  • より複雑な名前の型
  • 名前のない(または名前を知ることが難しい)型
  • 多くの場合利便性の都合で導入される間接的な型

特に、C++0xのラムダ関数が生成する関数オブジェクトを考えてみよう。一般的にその型を書き出すのは困難だ。だからautoを使わずにその関数オブジェクトを保持したいなら、間接的な型を使わざるを得ない:

function<void(void)> f = [] { DoSomething(); };  // ラッパーで保持する:間接的な型が必要
auto f = [] { DoSomething(); }; 	// 型を推論して直接束縛

この後の方の書き方は、関数ポインタを用いたC言語の等価なコードより効率的である点にご注目。なぜならC++はあらゆるコードをインライン化させてくれるからだ。詳しくはScott Meyersの"Effective STL"第46項の「関数よりも関数オブジェクトを使おう」を見て欲しいが、つまり(直感に反して)このほうが効率的なのだ。

さて、autoやvarの素晴らしさに疑問の余地はないとはいえ、これらにも些細な制限がある。とりわけ正確な型が必要なわけでなく、他の変換可能な型が欲しい場合がそうだ:

map<int,list<string>>::const_iterator ci = m.begin();    // ciの型はmap<int,...>
auto i = m.begin();                                      // iの型はmap<int,...>
Widget* w = new Widget();                                // wの型はWidget*
const Widget* cw = new Widget();                         // cwの型はconst Widget*
WidgetBase* wb = new Widget();                           // wbの型はWidgetBase*
shared_ptr<Widget> spw(new Widget());                    // spwの型はshared_ptr<...>
auto w = new Widget();                                   // wの型はWidget*

つまりC++0xのautoは(C#のvarと同様に)もっとも明確な型を得ることしかできない。とはいえ、これでほとんどのケースはカバーできる。

上記の例のすべてについて注意しておいて欲しい重要な点は、君がどう綴ろうと、すべての変数は明確で、あいまい性がなく、既知で、予測可能な静的な型を持つということだ。C++0xのautoやC#のvarは純粋に記法上の便宜であって、我々が自分で型名を綴る必要性から救ってくれる。しかし変数がたった一つの固定された静的な型を持つということは変わらない。

静的型付けと動的型付け

上記の引用でJeffが正しく指摘しているように、これは動的型付けではない。動的型付けとは、同じ変数がその生存期間の別の時点において異なる実際の型を持つことを許す、ということだ。残念ながら彼はこう続けてしまっているが、これは読者に別のことを想起するというミスを引き起こしかねない:

暗黙的な変数の型付けは、より動的な型付け言語へと至る入門用の麻薬だ、とさえ言い出す人もいるだろう。

同じ記事の中でJeffが書いていることは正しいから、彼が何について語っているのか理解していることは分かっている。しかし、これはハッキリさせておいた方が良い:型推論は動的型付けとは関係ない。
Jeffもちゃんと注意している。型推論は、いつも君が動的型付け言語でやっているように変数を宣言できるようにするだけのことだ、と。(このポストをする前に、私は"Lambda the Ultimate"でも同じ混乱に基づいたコメントを目にした。少なくともコメントした一人は、これは静的型付け言語への入門用麻薬だとみなすこともできると述べている。なぜなら、静的型付けを放棄せずに記述上の便宜を得ることも可能だからだ。)

Bjarneの用語集から引用すると:

動的型付け
オブジェクトの型が実行時に決定される;例えば、dynamic_castやtypeidを用いること。most-derived type(訳注:最後に派生された型)とも。
静的型付け
オブジェクトの型がその宣言を元にコンパイラに知られる。動的型付けも参照。

先ほどのC++の例に戻ろう。この例では変数の動的型と静的型の違いを示している。

WidgetBase* wb = new Widget(); // wbの静的型はWidgetBase*
if( dynamic_cast<Widget*>(wb ) ){ ... } // キャストは成功する:wbの動的型はWidget*

変数の静的な型は、それがサポートしているインタフェースを述べている。だからこのケースでは、wbはWidgetBaseのメンバしかアクセスさせてくれない。変数の動的な型は、まさにその時点でその変数が指しているオブジェクトの型である。

しかし、動的に型付けされた言語では、変数は静的な型を持たず、意識することもない。多くの動的言語では、変数を宣言する必要すらない。例えば:

x = 10;  // xの型はint
x = "hello, world"; // xの型はstr

Boostのvariantとany

言語として静的に型付けられたままのC++で同様の効果を得るには、二通りの方法がある。一つ目は、Boostのvariantだ:

// C++ using Boost
variant< int, string > x;   // 許される型を宣言する
x = 42;                     // x は int を保持する
x = "hello, world";         // x は string を保持する
x = new Widget();           // int でも string でもないのでエラー

unionとは異なり、variantは基本的にあらゆる種類の型を含めることができるが、型は前もって定義されている必要がある。オーバーロードの解決をシミュレートすることも可能で、boost::apply_visitorを用いる。これは静的に(コンパイル時に)チェックされる。
二つ目はBoost anyだ:

// C++ using Boost
any x;
x = 42;                     // x は int を保持する
x = "hello, world";         // x は string を保持する
x = new Widget();           // x は Widget* を保持する

これまたunionとは異なり、あらゆる型をanyに含めることができる。ところがvariantoとは異なり、anyは前もって型を定義することを強要しない(してくれない)。この良し悪しは、君が型付けをどこまで緩和したいかに寄って決まる。また、anyはオーバーロードの解決をシミュレートする手段がなく、保持するオブジェクトのためにヒープ領域を常に必要とする。

面白いことに、このことはC++がいかに巧みに、しっかりと(しかも効率を置き去りにせずに)「可能な限り静的型付けで、必要なら動的型付けで」への道を進んでいるかを示している。

次のようなケースではvariantを使おう:

  • 指定した型の組のいずれかひとつの値を保持するオブジェクトが欲しいとき
  • オーバーロードの解決をコンパイル時にチェックしたいとき
  • 可能な場合はスタック領域を使う(動的アロケーションのオーバーヘッドを回避する)ことで効率的にしたいとき
  • 正確に正しい型を使わなかった場合に恐ろしげなエラーメッセージを出してくれて構わないとき

次のようなケースではanyを使おう:

  • 仮想的に「あらゆる(any)」型の値を保持できるオブジェクトを使って、柔軟性を持たせたいとき
  • any_castの柔軟性を当てにしたいとき
  • swapに対して例外を送出しない保証が欲しいとき