型推論

型推論(かたすいろん、: type inference)とはプログラミング言語の機能の1つで、静的な型付けを持つ言語において、変数関数シグネチャを明示的に宣言しなくても、変数宣言における初期化のための初期値や、関数呼び出しにおける実引数などといった、周辺情報および文脈などから自動的に(暗黙的に)各々の型を決定する機構のこと。言語によってはtype deductionと呼ばれることもある。

推論に失敗するとその時点でエラーを報告できるため、少なくとも誤った型を用いることによるバグは回避できる。また、アルゴリズムの記述に集中できるのでプログラムの抽象度が上がるというメリットもある。型名が長大な場合に、型推論による省略によってコード全体の見通しをよくすることにもつながるが、一方で統合開発環境による支援(コードエディター上のツールチップなど)が得られない環境では、一見して型が分からないことでコードレビューがしにくくなるというデメリットもある。

代表的な型推論アルゴリズムとして、Hindley/Milner 型推論アルゴリズムがある。各々著名なコンピュータ科学者の名前からつけられた名前であるが、Hindley は論理学者として型推論システムを先に開発した。

型推論を持つ言語としてはHaskellMLValaOCamlF#C#JavaScalaC++D言語Concurrent CleanSwiftなどがある。静的型付け関数型プログラミング言語のほとんどがなんらかの型推論の機能を持っている。登場当初は型推論を持っていなかった言語であっても、関数型言語に影響を受けた拡張や改訂により型推論の機能を持つようになった言語も多い。

ただし型推論と関数の多重定義(オーバーロード)は相性が悪く、オーバーロードをサポートする言語では型推論による恩恵が十分に受けられない(型推論ではシグネチャを一意に決めることができない)ケースがある。

具体例による説明

この節では型推論の構文解析理論には踏み込んでいない。

ほとんどの言語においては、関数仮引数および戻り値演算子オペランドおよび結果、変数、そしてそれらから成るは、各々が保持するデータの種類を表すを持つ。構文上で明らかな名前を持つ型による区別をしない言語であっても、内部的にはなんらかの型を持っていて区別しているケースが多い。実行時に型が決まる言語を動的型付けの言語という。一方、コンパイル時に型が決まる言語を静的型付けの言語という。静的型付けの言語において、関数の仮引数および戻り値の型や変数の型は、通常は明示的に記述する必要がある。例えば、次はC言語の例である[1]

int addone(int x) {
    int result;
    result = x + 1;
    return result;
}

関数定義の最初の行int addone(int x)では、関数addoneは整数一つを入力引数として受け取り、整数を出力結果として返す、と宣言している。int result;の行では、ローカル変数resultが整数型であることを宣言している。

上記の例にほぼ1対1で対応するコードを、F#を使って記述すると下記のようになる。

let addone (x : int) : int =
    let result : int = x + 1
    result

しかしF#は型推論の機能を持っているため、次のように書くこともできる。

let addone x =
    let result = x + 1
    result

このF#の例において、

  • 2項演算子+の左オペランドと右オペランドの型は同じであり、演算結果は同じ型を返す。

という仕様であり、右オペランドには整数リテラル1が記述されていることから、左オペランドの変数xすなわち関数の引数xも同じ整数型であるということが推論される。これにより、式x + 1の値が整数型であることが型推論される。故にresultの型は整数であり、addone関数の戻り値の型が整数であることがわかる。

let y1 = addone 3
let y2 = addone 3.0 // double 型の値を渡すと、型の不一致によりコンパイルエラー。
let y3 = addone 3y // sbyte 型の値を渡すと、型の不一致によりコンパイルエラー。

なお、型推論はあくまで暗黙の型付けがなされるにすぎない。型推論によりコンパイル時に確定した型は不変である。

型推論のバリエーション

型推論により自動的に型を決定する機構は、変数宣言の際の暗黙的な型指定以外にも存在する。関数型言語ではほとんどの場面で型推論がサポートされるが、従来の手続き型言語オブジェクト指向言語でのサポートは言語および各言語の規格バージョンによってまちまちである。

変数宣言時の型推論

C#はバージョン3.0にて、varキーワードを用いたローカル変数の宣言時型推論を導入した。制約のひとつとして、宣言時初期化を伴う必要がある。for文foreach文、using文のスコープ変数にも利用できる。宣言時初期化文の右辺がメソッドグループや匿名関数(ラムダ式および匿名メソッド)の場合には適用できない[2]

// 型推論を用いない書き方。
string s1 = "文字列";
System.Console.WriteLine(s1.GetType()); // System.String
// 型推論を用いた書き方。
var s2 = "文字列";
System.Console.WriteLine(s2.GetType()); // System.String

var now = System.DateTime.Now;
System.Console.WriteLine(now.GetType()); // System.DateTime

now = s2; // コンパイルエラー。

var action1 = () => {}; // コンパイルエラー。
var action2 = delegate() {}; // コンパイルエラー。
var action3 = System.GC.Collect; // コンパイルエラー。
var action4 = new System.Action(() => {});
System.Action action5 = () => {};

var dict = new System.Collections.Generic.SortedDictionary<string, int> { {"Bravo", 0}, {"Alpha", 1}, {"Charlie", 2} };
foreach (var entry in dict) {
    //System.Diagnostics.Debug.Assert(entry is System.Collections.Generic.KeyValuePair<string, int>);
    System.Console.WriteLine("Key={0}, Value={1}", entry.Key, entry.Value);
}

この書き方はJavaScriptなど動的型付けの言語に非常によく似ているが、しかしながらすべての型はコンパイル時に定められる。また、バリアント型英語版とは異なり、実行時に再代入によって変数の中身の型が変わるようなことはない。

ラムダ式の仮引数の型を省略した場合も型推論が働く。戻り値の型は常に型推論によって決定される。

// ラムダ仮引数の型推論を用いない書き方。
System.Func<double, double> func1 = (double x) => x * x;
// ラムダ仮引数の型推論を用いた書き方。
System.Func<double, double> func2 = (x) => x * x;

Javaはバージョン8にてラムダ式を導入したが、C#同様に仮引数の型を省略すると型推論が働く。バージョン10にて、予約型名varによるローカル変数の宣言時型推論を導入した。バージョン11にて、ラムダ式の仮引数の型推論にもvarを使えるようになった。

C++C++11規格にて、キーワードautoおよびdecltypeによる一部の変数の宣言時型推論を導入した[3]。適用可能範囲はC#やJavaよりも広い。後継規格のC++14以降ではさらに適用可能範囲が広がっている。

namespace {
    auto g_variable = 0.0; // double
    struct MyType {
        static const auto s_variable = 0L; // long
    };
}
int main() {
    auto n = 0; // int
    decltype(n)* p1 = &n; // int*
    decltype(&n) p2 = &n; // int*
    decltype(n)& r1 = n; // int&
    decltype((n)) r2 = n; // int&
    auto f = []() {}; // コンパイラが生成する関数オブジェクト(クロージャ)型。
}

戻り値の型推論

C++C++11規格にて、キーワードautoおよびdecltypeによる戻り値の型推論を導入した[4]。後継規格のC++14ではdecltype(auto)による簡略表現もサポートする。

#include <iostream>
template<typename TFunc, typename TArg> auto invokeFunc(const TFunc& f, const TArg& a) -> decltype(f(a)) {
    return f(a);
}
int main() {
    std::cout << invokeFunc([](double x) { return x * x; }, 1.4142) << std::endl;
}

総称型の型推論

C++では、関数テンプレートに対してテンプレート実引数(具体的な型名)を明示的に与えて型を決定することもできるが、曖昧さがない場合に限り、関数呼び出しの実引数に応じて型を推論させることもできる。

#include <iostream>
#include <cmath>
template<typename T> T getVectorLength(T x, T y, T z) {
    return std::sqrt(x * x + y * y + z * z); // std::sqrt() には double あるいは float を受け取るオーバーロードが存在する。
}
int main() {
    const double len1 = getVectorLength<double>(1, 2, 3); // double getVectorLength(double, double, double)
    const float len2 = getVectorLength(1.0f, 2.0f, 3.0f); // float getVectorLength(float, float, float)
    std::cout << len1 << std::endl;
    std::cout << len2 << std::endl;
}

テンプレート仮引数Tを持つ関数テンプレートにおける仮引数の型宣言が、参照T&あるいはポインタT*であったり、ユニバーサル参照T&&であったりする場合は、推論の結果として定まる型が異なる場合もある。

C++17ではクラステンプレートのテンプレート引数を推論することもできるようになった[5]

template<typename T> struct Vector3 {
    T x, y, z;
    Vector3(T ax, T ay, T az) : x(ax), y(ay), z(az) {}
};
int main() {
    Vector3<double> v1(1.0, 2.0, 3.0); // C++03 以前でも利用可能な、従来のコンストラクタ呼び出しによる実体化。
    Vector3<double> v2 { 1.0, 2.0, 3.0 }; // C++11 以降の uniform initialization を使用した実体化。
    Vector3 v3(1.0, 2.0, 3.0); // C++17 以降でのみ有効。Vector3<double> に推論される。
    Vector3 v4 { 1.0, 2.0, 3.0 }; // 同上。
}

Javaはバージョン5.0以降にてメソッドスコープの型変数を推論する機能を持つ。

// 型推論を用いない書き方。
List<String> list1 = Collections.<String>emptyList();
// 型変数へのバインドに型推論を用いた書き方。
List<String> list2 = Collections.emptyList();

その他、Java 7 からは型変数を持つクラスをnewする場合にバインドすべき型を推論するダイヤモンド演算子という機能を持つ。

// 型推論を用いない書き方。
List<String> list1 = new ArrayList<String>();
// ダイヤモンド演算子による型推論。
List<String> list2 = new ArrayList<>();

無名関数の型推論の例

無名関数の型推論においては、複雑な状況が発生する。

C#の例を以下に示す。

// 複数のデリゲート型を定義
delegate void TwoStringAction(string left, string right);
delegate void OneParamAction(object o);
delegate void TwoParamAction(object o, EventArgs e);
delegate void TwoIntegerAction(int x, int y);

// メソッドのオーバーロードを用意する。有効化するオーバーロードの種類により、型推論の可否が変化する。
static void SomeMethod(TwoStringAction action) { /* Pattern 1 */ }
//static void SomeMethod(OneParamAction action) { /* Pattern 2 */ }
//static void SomeMethod(TwoParamAction action) { /* Pattern 3 */ }
//static void SomeMethod(TwoIntegerAction action) { /* Pattern 4 */ }

static void Main() {
    // メソッドのオーバーロードがPattern 1のみの場合、全ての文が有効(型推論可能)である。
    SomeMethod((o, e) => { /*No-op*/ }); /* 1行目 */
    SomeMethod((o, e) => { o = o + e; }); /* 2行目 */
    SomeMethod((o, e) => { o = "" + o + e; }); /* 3行目 */
    SomeMethod(delegate { /*No-op*/ }); /* 4行目 */
}
  • SomeMethod(OneParamAction action) の行を有効にすると、Mainメソッドの4行目の型推論が効かなくなる。
    • 4行目の匿名関数は引数リストを省略できるため、1引数、2引数のどちらのデリゲート型か曖昧になる。
  • SomeMethod(TwoParamAction action) の行を有効にすると、Mainメソッドの2行目以外の型推論が効かなくなる。
    • 1行目のラムダは、2引数で値を返さない複数のデリゲート型がある場合、曖昧となる。
    • 3行目のラムダは、代入式の左辺にあるostringを格納できる型でなければならないが、
      一方で右辺のoeは暗黙にToString()が呼び出されるため、いずれの型であってもよく、TwoStringAction TwoParamActionの間で曖昧となる。
    • 2行目のラムダは、oeの間に適切なoperator+が定義されなければならず、TwoStringAction型と推論される。
  • SomeMethod(TwoIntegerAction action) の行を有効にすると、Mainメソッドの3行目以外の型推論が効かなくなる。
    • 2行目のラムダは、oestringintのいずれでもoperator+が実行できるため、TwoStringActionTwoIntegerActionの間で曖昧となる。
    • 3行目のラムダは、代入式の左辺にあるostringを格納できる型でなければならないため、TwoStringAction型と推論される。

デリゲート型を引数に取るメソッドを複数用意する場合、オーバーロードではなく別名のメソッドとすることで、この複雑性は回避できる。

動的型言語における型推論

動的に型付けを行う言語の場合、文法的には型付けが行われず、あらゆる型の可能性を考慮して処理を進める必要があるため、処理が遅くなる原因となる[6]JITコンパイラによって高速化を図る場合、型推論によりあるデータを「特定の型」として扱うことが可能であれば、その型に合わせた処理だけをすることで高速化が行える[6]

JavaScript では、Webブラウザの分野では高速化が特に求められている[7]ため、2011年12月20日にリリースされたFirefox 9から高速化のため型推論技術を採用している[8]

Groovy 2.0 ではコンパイル時型検査 @TypeChecked の機能をつけたが、型を指定していない変数に対しても、型推論を利用して変数に型を割り振り、型検査を行っている。同様に Groovy に対応した IntelliJ IDEA などの統合開発環境も型推論を利用してアシストを行っている。

脚注

  1. ^ C言語は関数の戻り値の型を省略した場合、intを返すと仮定する仕様になっているが、これを型推論とは呼ばない。
  2. ^ 暗黙的に型指定されるローカル変数 - C# プログラミング ガイド | Microsoft Docs
  3. ^ auto - cpprefjp C++日本語リファレンス
  4. ^ decltype - cpprefjp C++日本語リファレンス
  5. ^ クラステンプレートのテンプレート引数推論 - cpprefjp C++日本語リファレンス
  6. ^ a b Type Inference brings JS improvements to Firefox Beta Brian Hackett, 2011年11月10日(2011年12月24日閲覧)。
    型推論により Firefox Beta の JavaScript が高速化しました(上の記事の和訳)、2011年12月24日閲覧。
  7. ^ 一色政彦 (2011年4月22日). “Internet Explorer 9正式版レビュー”. @IT. December 24, 2011閲覧。
  8. ^ Mozilla、「Firefox 9」の正式版をリリース 「型推論」技術で45%高速に”. ITmedia (2011年12月21日). December 24, 2011閲覧。