型推論(Type Inference)は、プログラミング言語において、変数や式の型を明示的に指定せずに、コンパイラやインタプリタがその型を推測する機能です。型推論により、プログラマはコードを簡潔に書くことができ、同時に型安全性を保証できます。

型推論は、静的型付け言語や動的型付け言語の両方で利用されますが、特に静的型付け言語でよく使用されます。静的型付け言語では、変数や関数の型がコンパイル時に決定されるため、型推論は型の冗長な指定を省略し、コードの可読性を向上させます。

型推論の機能は言語ごとに異なりますが、一般的には以下のような手法が使われます。

ヒントに基づく推論(Type Hinting)
プログラマが型のヒントを与えることで、コンパイラやインタプリタがその型を推論します。これにより、型の一部を明示的に指定しながら、型の推論をサポートすることができます。
制約ベースの型推論(Constraint-Based Type Inference)
制約ソルバーを使用して、変数や式の型に関する制約を解決し、最適な型を推論します。制約ベースの型推論は、Hindley-Milner型システムなどの多くの型推論アルゴリズムで使用されます。
ユニフィケーションベースの型推論(Unification-Based Type Inference)
型の等式制約を解くために、ユニフィケーションアルゴリズムを使用します。この手法は、Prologなどの論理プログラミング言語や、ML系言語などで使用されます。

型推論の利点は多岐にわたります。まず第一に、コードの記述量を減らし、タイプミスや型不一致によるバグを減らすことができます。さらに、型の推論により、型安全性が向上し、コードの品質が向上します。ただし、型推論が複雑な場合、コンパイル時間が長くなる場合があります。また、推論された型が意図しないものになることもありますので、プログラマは注意が必要です。


型推論とプログラミング言語 編集

型推論を実装したプログラミング言語にはさまざまなアルゴリズムがあります。 以下にいくつかの代表的なアルゴリズムとそれを採用している言語を示します。

Hindley-Milner型推論
主な特徴
多くの静的型付け言語で採用されており、単相型(monomorphic type)の推論に使用される。
実装例
ML系言語(Standard ML、OCaml)、Haskell、Scala(一部)
Damas-Hindley-Milner型推論
主な特徴
Hindley-Milner型推論を拡張し、多相型(polymorphic type)の推論を行う。
実装例
Haskell、MLton、Miranda
Algorithm W
主な特徴
Hindley-Milner型推論の特殊なケースであり、変数の一般化(generalization)と単一化(unification)を行う。
実装例
ML系言語(Standard ML、OCaml)、Haskell、Scala
Bidirectional型推論
主な特徴
型の推論を前方向と後方向の2つのフェーズに分け、より柔軟な型推論を可能にする。
実装例
TypeScript、Scala、Purescript

これらのアルゴリズムは、静的型付け言語において一般的に使用されており、それぞれ異なる特性や利点を持っています。

例えば、Hindley-Milner型推論は単相型の推論に優れており、Algorithm Wはその特殊なケースを扱うことができます

。一方、Bidirectional型推論は、より柔軟な型システムを提供するために、前方向と後方向のフェーズを組み合わせています。

言語の型推論そのアルゴリズム 編集

以下に、各言語ごとにコード例とその型推論アルゴリズムを解説します。

  1. Haskell (Hindley-Milner型推論):
-- 関数の例
add x y = x + y
  1. この例では、関数 add の型を推論します。Hindley-Milner型推論により、xy の型が数値型であることが推論され、関数の型は Num a => a -> a -> a となります。ここで、Num a => は型クラス制約を示し、a任意の数値型を表します。この推論は、変数や式の型を利用して、最も一般的な型を推測することで行われます。
  2. Scala (Hindley-Milner型推論の一部を使用):
// 関数の例
def add(x: Int, y: Int) = x + y
  1. Scalaでは、一部の場合にHindley-Milner型推論が使用されますが、その他の場合にはより複雑な型推論アルゴリズムが使用されます。この例では、関数 add の引数 xy の型が Int であることが明示されています。そのため、この関数の型は Int => Int => Int と推論されます。この推論は、式の型や型アノテーションを使用して行われます。
  2. TypeScript (Bidirectional型推論):
// 関数の例
function add(x, y) {
    return x + y;
}
  1. TypeScriptでは、Bidirectional型推論が使用されます。この例では、関数 add の引数 xy の型が明示されていませんが、関数内での演算 x + y から、引数の型が数値であることが推測されます。そのため、この関数の型は (x: number, y: number) => number と推論されます。この推論は、関数の呼び出し元や関数内の式を使用して行われます。
-- 関数の例
add x y = x + y

Haskell 編集

Haskellでは、関数の引数や戻り値の型を指定することなく、コンパイラが自動的に型を推論します。

Haskellの型推論の例
-- リストの型推論の例
myList = [1, 2, 3, 4, 5] -- myListはNum a => [a]型と推論される

-- 関数の型推論の例
add x y = x + y -- add関数の型はNum a => a -> a -> aと推論される

-- タプルの型推論の例
myTuple = (1, "hello", True) -- myTupleは(Num a, IsString b) => (a, [Char], Bool)型と推論される

-- ジェネリクスの型推論の例
myMaybe = Just 42 -- myMaybeはNum a => Maybe a型と推論される

-- 関数の型推論の例
myFunc x = x * 2 -- myFuncはNum a => a -> a型と推論される
このコードでは、関数addの引数xyの型を指定していません。しかし、コンパイラはNum型クラスのインスタンスである型を推論し、正しい結果を得ることができます。

OCaml 編集

OCamlの型推論では、変数に対して型を明示的に指定する必要はありません。

OCamlの型推論の例
(* リストの型推論の例 *)
let my_list = [1; 2; 3; 4; 5] (* my_listはint list型と推論される *)

(* 関数の型推論の例 *)
let add x y = x + y (* add関数の型はint -> int -> intと推論される *)

(* タプルの型推論の例 *)
let my_tuple = (1, "hello", true) (* my_tupleはint * string * bool型と推論される *)

(* ジェネリクスの型推論の例 *)
let my_maybe = Some 42 (* my_maybeはint option型と推論される *)

(* 関数の型推論の例 *)
let my_func x = x * 2 (* my_funcはint -> int型と推論される *)

C++ 編集

C++では、autoキーワードを使用して、変数の型を推論することができます。

C++の型推論の例
#include <iostream>
#include <vector>
#include <map>
#include <functional>

int main() {
    // ベクターの型推論の例
    auto myVector = std::vector<int>{1, 2, 3, 4, 5}; // myVectorはstd::vector<int>型と推論される

    // 関数の型推論の例
    auto add = [](int x, int y) { return x + y; }; // add関数の型は自動的に推論される

    // マップの型推論の例
    auto myMap = std::map<std::string, int>{{"a", 1}, {"b", 2}, {"c", 3}}; // myMapはstd::map<std::string, int>型と推論される

    // 関数ポインタの型推論の例
    auto myFunc = [](int x) -> int { return x * 2; }; // myFuncはstd::function<int(int)>型と推論される

    // ラムダ式の型推論の例
    auto myLambda = [](int x) { return x * x; }; // myLambdaはstd::function<int(int)>型と推論される

    std::cout << "Vector: ";
    for (auto& num : myVector) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::cout << "Map: ";
    for (const auto& pair : myMap) {
        std::cout << "{" << pair.first << ": " << pair.second << "} ";
    }
    std::cout << std::endl;

    std::cout << "Result of add(3, 4): " << add(3, 4) << std::endl;

    std::cout << "Result of myFunc(5): " << myFunc(5) << std::endl;

    std::cout << "Result of myLambda(6): " << myLambda(6) << std::endl;

    return 0;
}
この例では、my_variableint型であることが推論されます。

Rust 編集

Rustでは、変数の型を指定することなく、コンパイラが自動的に型を推論します。

Rustでの型推論の例
fn main() {
    // ベクターの型推論の例
    let my_vector = vec![1, 2, 3, 4, 5]; // my_vectorはVec<i32>型と推論される

    // 関数の型推論の例
    let add = |x: i32, y: i32| x + y; // add関数の型は自動的に推論される

    // ハッシュマップの型推論の例
    let mut my_map = std::collections::HashMap::new();
    my_map.insert(String::from("a"), 1);
    my_map.insert(String::from("b"), 2);
    my_map.insert(String::from("c"), 3); // my_mapはHashMap<String, i32>型と推論される

    // クロージャの型推論の例
    let my_closure = |x: i32| x * 2; // my_closureはクロージャ型と推論される

    // Option<i>型の型推論の例
    let my_maybe = Some(42); // my_maybeはOption<i32>型と推論

    println!("Vector: {:?}", my_vector);
    for (key, value) in &my_map {
        println!("Key: {}, Value: {}", key, value);
    }
    println!("Result of add(3, 4): {}", add(3, 4));
    println!("Result of my_closure(5): {}", my_closure(5));
}

TypeScript 編集

TypeScriptは静的型付け言語であり、変数の型を指定する必要がありますが、型推論によりコンパイラが自動的に型を推論することができます。

TypeScriptでの型推論の例
// 配列の型推論の例
const myArray = [1, 2, 3, 4, 5]; // myArrayはnumber[]型と推論される

// 関数の型推論の例
function add(x: number, y: number) {
    return x + y;
} // add関数の型は(x: number, y: number) => numberと推論される

// オブジェクトの型推論の例
const myObject = { a: 1, b: 2, c: 3 }; // myObjectは{ a: number, b: number, c: number }型と推論される

// ジェネリクスの型推論の例
const myArray2 = [1, 2, 3, 4, 5]; // myArray2はnumber[]型と推論される
const myOption = myArray2.find(x => x === 3); // myOptionはnumber | undefined型と推論される

// 関数の型推論の例
const myFunc = (x: number) => x * 2; // myFuncは(x: number) => number型と推論される
このコードでは、関数addの引数xyの型を指定していませんが、コンパイラはそれらをnumber型と推論します。

Scala 編集

Scalaは静的型付け言語であり、変数の型を指定する必要がありますが、型推論によりコンパイラが自動的に型を推論することができます。

Scalaでの型推論の例
object Main extends App {
  // リストの型推論の例
  val myList = List(1, 2, 3, 4, 5) // myListはList[Int]型と推論される

  // 関数の型推論の例
  val add = (x: Int, y: Int) => x + y // add関数の型は(Int, Int) => Intと推論される

  // マップの型推論の例
  val myMap = Map("a" -> 1, "b" -> 2, "c" -> 3) // myMapはMap[String, Int]型と推論される

  // ジェネリクスの型推論の例
  val myOption = Some(42) // myOptionはOption[Int]型と推論される

  // 関数リテラルの型推論の例
  val myFunc = (x: Int) => x * 2 // myFuncはInt => Int型と推論される

  println("myList: " + myList)
  println("Result of add(3, 4): " + add(3, 4))
  println("myMap: " + myMap)
  println("myOption: " + myOption)
  println("Result of myFunc(5): " + myFunc(5))
}
このコードでは、関数addの引数xyの型を指定していますが、変数xyの型を指定していません。しかし、コンパイラはそれらをInt型と推論します。

Kotlin 編集

Kotlinは静的型付け言語であり、変数の型を指定する必要がありますが、型推論によりコンパイラが自動的に型を推論することができます。

Kotlinでの型推論の例
fun main() {
    // リストの型推論の例
    val myList = listOf(1, 2, 3, 4, 5) // myListはList<Int>型と推論される

    // 関数の型推論の例
    val add = { x: Int, y: Int -> x + y } // add関数の型は(Int, Int) -> Intと推論される

    // マップの型推論の例
    val myMap = mapOf("a" to 1, "b" to 2, "c" to 3) // myMapはMap<String, Int>型と推論される

    // 関数リテラルの型推論の例
    val myFunc = { x: Int -> x * 2 } // myFuncは(Int) -> Int型と推論される

    println("myList: $myList")
    println("Result of add(3, 4): ${add(3, 4)}")
    println("myMap: $myMap")
    println("myNullable: $myNullable")
    println("Result of myFunc(5): ${myFunc(5)}")
}

Swift 編集

Swiftでは、変数に対して明示的に型を指定することができますが、通常は必要ありません。

Swiftの型推論の例
import Foundation

// 配列の型推論の例
let myList = [1, 2, 3, 4, 5] // myListは[Int]型と推論される

// 関数の型推論の例
let add = { (x: Int, y: Int) -> Int in return x + y } // add関数の型は(Int, Int) -> Intと推論される

// 辞書の型推論の例
let myDict = ["a": 1, "b": 2, "c": 3] // myDictは[String: Int]型と推論される

// クロージャの型推論の例
let myClosure = { (x: Int) -> Int in return x * 2 } // myClosureは(Int) -> Int型と推論される

print("myList: \(myList)")
print("Result of add(3, 4): \(add(3, 4))")
print("myDict: \(myDict)")
print("myOptional: \(String(describing: myOptional))")
print("Result of myClosure(5): \(myClosure(5))")
この例では、my_variableString型であることが推論されます。

Go 編集

Goでも、変数の型を明示的に指定することができますが、通常は必要ありません。

Goの型推論の例
package main

import "fmt"

func main() {
    // スライスの型推論の例
    myList := []int{1, 2, 3, 4, 5} // myListは[]int型と推論される

    // 関数の型推論はありませんが、無名関数を使うことで型の指定を省略できます。
    add := func(x, y int) int { return x + y } // add関数の型は明示的に指定されています

    // マップの型推論の例
    myMap := map[string]int{"a": 1, "b": 2, "c": 3} // myMapはmap[string]int型と推論される

    // 変数の型推論の例
    var myVariable = 42 // myVariableはint型と推論される

    fmt.Println("myList:", myList)
    fmt.Println("Result of add(3, 4):", add(3, 4))
    fmt.Println("myMap:", myMap)
    fmt.Println("myVariable:", myVariable)
}
この例では、my_variablestring型であることが推論されます。

Zig 編集

Zigでも、変数の型を明示的に指定することができますが、通常は必要ありません。

Zigの型推論の例
const my_variable = 42;
この例では、my_variableconst int型であることが推論されます。const intは、int型の定数を表します。