Wonderfull Rust

型システム

Rust の型システムの位置づけ

Rust の型システムは,ML 系言語(OCaml, Haskell)の理論的基盤と,C++ のシステムプログラミングの実用性を融合させたものだ.静的型付け,型推論,パラメトリック多相(ジェネリクス),アドホック多相(トレイト)を兼ね備え,さらに所有権とライフタイムという独自の概念を型レベルで表現する.

Hindley-Milner 型推論

Rust の型推論は Hindley-Milner (HM) 型推論 をベースにしている.HM 型推論は ML 系言語で広く使われているアルゴリズムで,プログラム中の型注釈を最小限に抑えつつ,すべての式の型をコンパイル時に決定する.

fn main() {
    let x = 42;                    // x: i32 と推論される
    let y = 3.14;                  // y: f64 と推論される
    let v = vec![1, 2, 3];         // v: Vec<i32> と推論される
    let s = String::from("hello"); // s: String と推論される

    // 使い方から型が逆方向に推論される
    let mut nums = Vec::new();     // この時点では Vec<_> (型未確定)
    nums.push(42);                 // ← ここで Vec<i32> と確定
}

特に最後の例が重要だ.Vec::new() の呼び出し時点では要素の型は決まらないが,後続の push(42) により i32 が要素型であることが 逆方向に 推論される.これは HM 型推論の 制約ベースの統一化 (unification) によるものだ.

ローカル型推論

Rust の型推論は原則として ローカル に行われる.関数のシグネチャ(引数の型と戻り値の型)は必ず明示する必要があり,型推論は関数本体の中で閉じている:

// 関数シグネチャでは型注釈が必須
fn add(a: i32, b: i32) -> i32 {
    a + b  // 本体内の型は推論される
}

これは,Haskell がトップレベルの型も推論できるのとは異なる.Rust がローカル型推論を選んだ理由は:

  1. コンパイル速度: 関数単位で型チェックを並列実行できる
  2. 可読性: 関数のインターフェースが常に明示的で,コードを読む際に関数シグネチャを見れば型がわかる
  3. エラーメッセージの質: 推論のスコープが限定されるため,型エラーの原因を特定しやすい

turbofish 構文

型推論で型が確定しない場合,明示的に型を指定する必要がある:

// 型推論だけでは確定しない場合
let x = "42".parse::<i32>().unwrap();   // turbofish ::<>
let y: i32 = "42".parse().unwrap();     // 変数の型注釈

// collect は特に型注釈が必要になることが多い
let v: Vec<i32> = (0..10).collect();
let v = (0..10).collect::<Vec<i32>>();

::<Type> という構文は turbofish と呼ばれ,ジェネリックなメソッドの型パラメータを明示的に指定するために使われる.

ジェネリクス

パラメトリック多相

ジェネリクス(パラメトリック多相)は,型をパラメータとして抽象化するメカニズムだ:

// T は任意の型を表す型パラメータ
fn first<T>(items: &[T]) -> Option<&T> {
    items.first()
}

struct Pair<A, B> {
    first: A,
    second: B,
}

enum Either<L, R> {
    Left(L),
    Right(R),
}

トレイト境界 (Trait Bounds)

型パラメータに対して,「この型はこのトレイトを実装していなければならない」という制約を課すことができる:

// T は Display を実装した型でなければならない
fn print_twice<T: std::fmt::Display>(value: T) {
    println!("{}", value);
    println!("{}", value);
}

// 複数のトレイト境界
fn process<T: Clone + std::fmt::Debug + PartialEq>(value: T) {
    let cloned = value.clone();
    println!("{:?}", cloned);
}

// where 句による読みやすい記法
fn complex_function<T, U>(t: T, u: U) -> String
where
    T: Display + Clone,
    U: Debug + Into<String>,
{
    format!("{}: {:?}", t, u)
}

トレイト境界はジェネリクスの表現力を大きく高める.「任意の型」ではなく「特定の能力を持つ任意の型」を表現できるため,型安全でありながら汎用的な関数やデータ構造を定義できる.

impl Trait

impl Trait は,ジェネリクスの糖衣構文として,または戻り値の型を隠蔽するために使われる:

// 引数位置: ジェネリクスの糖衣構文
fn print_item(item: &impl Display) {
    println!("{}", item);
}
// ↑ これは以下と同等
fn print_item_expanded<T: Display>(item: &T) {
    println!("{}", item);
}

// 戻り値位置: 具体的な型を隠蔽
fn make_greeting(name: &str) -> impl Display {
    format!("Hello, {}!", name)
}

戻り値位置の impl Trait は特に重要だ.具体的な型(ここでは String)を公開せずに「Display を実装した何らかの型」としてのみ公開できる.これにより,内部実装の変更が API の互換性を壊さなくなる.

関連型 (Associated Types)

トレイトは型パラメータの代わりに 関連型 を持つことができる:

trait Iterator {
    type Item;  // 関連型

    fn next(&mut self) -> Option<Self::Item>;
}

関連型とジェネリック型パラメータの使い分けは重要だ:

  • 関連型: 実装者が型を一意に決定する場合.ある型に対してトレイトの実装は一つだけ
  • 型パラメータ: 同じ型に対して複数の実装がありうる場合
// 関連型: Vec<T> の Iterator 実装は Item = T で一意に決まる
impl<T> Iterator for VecIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<T> { /* ... */ }
}

// 型パラメータ: 同じ型に対して複数の From を実装できる
impl From<i32> for MyType { /* ... */ }
impl From<String> for MyType { /* ... */ }

GATs (Generic Associated Types)

GATs (Rust 1.65 で安定化) は,関連型にジェネリックパラメータを持たせる機能だ:

trait LendingIterator {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

GATs が解決する問題を理解するために,GATs がなかった場合を考えてみよう:

// GATs なし: 自分自身への参照を返すイテレータが書けなかった
trait StreamingIterator {
    type Item;  // ← ライフタイムパラメータを持てない

    // Item が &self を借用していることを表現できない
    fn next(&mut self) -> Option<Self::Item>;
}

// GATs あり: ライフタイムがパラメータ化される
trait LendingIterator {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

struct WindowIter<'s> {
    data: &'s [i32],
    pos: usize,
}

impl<'s> LendingIterator for WindowIter<'s> {
    type Item<'a> = &'a [i32] where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<&'a [i32]> {
        if self.pos + 2 <= self.data.len() {
            let window = &self.data[self.pos..self.pos + 2];
            self.pos += 1;
            Some(window)
        } else {
            None
        }
    }
}

GATs により,「メソッド呼び出しごとに異なるライフタイムを持つ関連型」を表現できるようになった.これは,ストリーミングイテレータ,非同期トレイト,コレクション抽象化など,多くの高度なパターンの基盤となる.

GATs の型パラメータ

GATs はライフタイムだけでなく,型パラメータも持つことができる:

trait Container {
    type Item<T>;

    fn wrap<T>(&self, value: T) -> Self::Item<T>;
}

型レベルのプログラミング

Rust の型システムでは,型レベルで情報を表現し,コンパイル時に検証することができる:

Newtype パターン

struct Meters(f64);
struct Kilometers(f64);

// Meters と Kilometers を混同するとコンパイルエラーになる
fn distance_in_meters(d: Meters) -> f64 {
    d.0
}

// distance_in_meters(Kilometers(5.0));  // コンパイルエラー!

同じ f64 でも,意味の異なる値を型レベルで区別できる.ランタイムコストはゼロ(単相化により Newtype のラッパーは消去される).

PhantomData による型レベルマーカー

use std::marker::PhantomData;

struct Authenticated;
struct Unauthenticated;

struct Session<State> {
    token: String,
    _state: PhantomData<State>,
}

impl Session<Unauthenticated> {
    fn login(token: String) -> Session<Authenticated> {
        Session {
            token,
            _state: PhantomData,
        }
    }
}

impl Session<Authenticated> {
    fn get_secret_data(&self) -> &str {
        "secret"
    }
}

// 未認証セッションから秘密データにアクセスすることは型レベルで不可能

PhantomData は実行時にはサイズゼロだが,型パラメータを通じて状態を型レベルで表現する.型状態パターン (Typestate Pattern) と呼ばれるこのテクニックにより,「不正な状態をコンパイル時に排除する」という設計が可能になる.

まとめ

Rust の型システムは,以下の特性の組み合わせによって強力さを実現している:

特性 内容
HM 型推論 関数本体内の型注釈を最小化
ジェネリクス 型安全な汎用プログラミング
トレイト境界 型パラメータの能力を制約
関連型 型パラメータの一意な決定
GATs 関連型のジェネリック化
単相化 ジェネリクスのゼロコスト実行
Newtype 型レベルの意味的区別

これらの機能は個々に見ても強力だが,組み合わせることで「不正な状態を型レベルで排除する」「コンパイル時にバグを発見する」「ランタイムコストなしで抽象化する」という Rust の設計目標を実現している.

本に戻る