型推論
型推論(かたすいろん、type inference)とはプログラミング言語の機能の1つで、静的な型付けを持つ言語において、変数や関数シグネチャの型を明示的に宣言しなくても、変数宣言における初期化のための初期値や、関数呼び出しにおける実引数などといった、周辺情報および文脈などから自動的に(暗黙的に)各々の型を決定する機構のこと。言語によってはtype deductionと呼ばれることもある。
推論に失敗するとその時点でエラーを報告できるため、少なくとも誤った型を用いることによるバグは回避できる。また、アルゴリズムの記述に集中できるのでプログラムの抽象度が上がるというメリットもある。型名が長大な場合に、型推論による省略によってコード全体の見通しをよくすることにもつながるが、一方で統合開発環境による支援(コードエディター上のツールチップなど)が得られない環境では、一見して型が分からないことでコードレビューがしにくくなるというデメリットもある。
代表的な型推論アルゴリズムとして、Hindley/Milner 型推論アルゴリズムがある。各々著名なコンピュータ科学者の名前からつけられた名前であるが、Hindley は論理学者として型推論システムを先に開発した。
型推論を持つ言語としてはHaskell、ML、Vala、OCaml、F#、C#、Java、Scala、C++、D言語、Concurrent Clean、Swiftなどがある。静的型付け関数型プログラミング言語のほとんどがなんらかの型推論の機能を持っている。登場当初は型推論を持っていなかった言語であっても、関数型言語に影響を受けた拡張や改訂により型推論の機能を持つようになった言語も多い。
ただし型推論と関数の多重定義(オーバーロード)は相性が悪く、オーバーロードをサポートする言語では型推論による恩恵が十分に受けられない(型推論ではシグネチャを一意に決めることができない)ケースがある。
この節では型推論の構文解析理論には踏み込んでいない。
ほとんどの言語においては、関数の仮引数および戻り値、演算子のオペランドおよび結果、変数、そしてそれらから成る式は、各々が保持するデータの種類を表す型を持つ。構文上で明らかな名前を持つ型による区別をしない言語であっても、内部的にはなんらかの型を持っていて区別しているケースが多い。実行時に型が決まる言語を動的型付けの言語という。一方、コンパイル時に型が決まる言語を静的型付けの言語という。静的型付けの言語において、関数の仮引数および戻り値の型や変数の型は、通常は明示的に記述する必要がある。例えば、次はC言語の例である。
関数定義の最初の行int addone(int x)では、関数addoneは整数一つを入力引数として受け取り、整数を出力結果として返す、と宣言している。int result;の行では、ローカル変数resultが整数型であることを宣言している。
上記の例にほぼ1対1で対応するコードを、F#を使って記述すると下記のようになる。
しかしF#は型推論の機能を持っているため、次のように書くこともできる。
このF#の例において、
2項演算子+の左オペランドと右オペランドの型は同じであり、演算結果は同じ型を返す。
という仕様であり、右オペランドには整数リテラル1が記述されていることから、左オペランドの変数xすなわち関数の引数xも同じ整数型であるということが推論される。これにより、式x + 1の値が整数型であることが型推論される。故にresultの型は整数であり、addone関数の戻り値の型が整数であることがわかる。
なお、型推論はあくまで暗黙の型付けがなされるにすぎない。型推論によりコンパイル時に確定した型は不変である。
型推論により自動的に型を決定する機構は、変数宣言の際の暗黙的な型指定以外にも存在する。関数型言語ではほとんどの場面で型推論がサポートされるが、従来の手続き型言語やオブジェクト指向言語でのサポートは言語および各言語の規格バージョンによってまちまちである。
C#はバージョン3.0にて、varキーワードを用いたローカル変数の宣言時型推論を導入した。制約のひとつとして、宣言時初期化を伴う必要がある。for文、foreach文、using文のスコープ変数にも利用できる。宣言時初期化文の右辺がメソッドグループや匿名関数(ラムダ式および匿名メソッド)の場合には適用できない。
この書き方はJavaScriptなど動的型付けの言語に非常によく似ているが、しかしながらすべての型はコンパイル時に定められる。また、バリアント型とは異なり、実行時に再代入によって変数の中身の型が変わるようなことはない。
ラムダ式の仮引数の型を省略した場合も型推論が働く。戻り値の型は常に型推論によって決定される。
Javaはバージョン8にてラムダ式を導入したが、C#同様に仮引数の型を省略すると型推論が働く。バージョン10にて、予約型名varによるローカル変数の宣言時型推論を導入した。バージョン11にて、ラムダ式の仮引数の型推論にもvarを使えるようになった。
C++はC++11規格にて、キーワードautoおよびdecltypeによる一部の変数の宣言時型推論を導入した。適用可能範囲はC#やJavaよりも広い。後継規格のC++14以降ではさらに適用可能範囲が広がっている。
C++はC++11規格にて、キーワードautoおよびdecltypeによる戻り値の型推論を導入した。後継規格のC++14ではdecltype(auto)による簡略表現もサポートする。
C++では、関数テンプレートに対してテンプレート実引数(具体的な型名)を明示的に与えて型を決定することもできるが、曖昧さがない場合に限り、関数呼び出しの実引数に応じて型を推論させることもできる。
テンプレート仮引数Tを持つ関数テンプレートにおける仮引数の型宣言が、参照T&あるいはポインタT*であったり、ユニバーサル参照T&&であったりする場合は、推論の結果として定まる型が異なる場合もある。
C++17ではクラステンプレートのテンプレート引数を推論することもできるようになった。
Javaはバージョン5.0以降にてメソッドスコープの型変数を推論する機能を持つ。
その他、Java 7 からは型変数を持つクラスをnewする場合にバインドすべき型を推論するダイヤモンド演算子という機能を持つ。
無名関数の型推論においては、複雑な状況が発生する。
C#の例を以下に示す。
SomeMethod(OneParamAction action) の行を有効にすると、Mainメソッドの4行目の型推論が効かなくなる。
4行目の匿名関数は引数リストを省略できるため、1引数、2引数のどちらのデリゲート型か曖昧になる。
SomeMethod(TwoParamAction action) の行を有効にすると、Mainメソッドの2行目以外の型推論が効かなくなる。
1行目のラムダは、2引数で値を返さない複数のデリゲート型がある場合、曖昧となる。
3行目のラムダは、代入式の左辺にあるoはstringを格納できる型でなければならないが、 一方で右辺のoとeは暗黙にToString()が呼び出されるため、いずれの型であってもよく、TwoStringAction TwoParamActionの間で曖昧となる。
2行目のラムダは、oとeの間に適切なoperator+が定義されなければならず、TwoStringAction型と推論される。
SomeMethod(TwoIntegerAction action) の行を有効にすると、Mainメソッドの3行目以外の型推論が効かなくなる。
2行目のラムダは、oとeはstringかintのいずれでもoperator+が実行できるため、TwoStringActionTwoIntegerActionの間で曖昧となる。
3行目のラムダは、代入式の左辺にあるoはstringを格納できる型でなければならないため、TwoStringAction型と推論される。
デリゲート型を引数に取るメソッドを複数用意する場合、オーバーロードではなく別名のメソッドとすることで、この複雑性は回避できる。
動的に型付けを行う言語の場合、文法的には型付けが行われず、あらゆる型の可能性を考慮して処理を進める必要があるため、処理が遅くなる原因となる。JITコンパイラによって高速化を図る場合、型推論によりあるデータを「特定の型」として扱うことが可能であれば、その型に合わせた処理だけをすることで高速化が行える。
JavaScript では、Webブラウザの分野では高速化が特に求められているため、2011年12月20日にリリースされたFirefox 9から高速化のため型推論技術を採用している。
Groovy 2.0 ではコンパイル時型検査 @TypeChecked の機能をつけたが、型を指定していない変数に対しても、型推論を利用して変数に型を割り振り、型検査を行っている。同様に Groovy に対応した IntelliJ IDEA などの統合開発環境も型推論を利用してアシストを行っている。