なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

Rustにおいてbreakやreturnを含む式はどの型であっても良い

  • 2022/8/27
    • 演習問題の(3)で提示されているコードが間違っていたので修正

Rustaceanなら周知の事実であるが、Rustは基本式指向の言語である。そのため、すべてのブロックは式として扱われる。そしてとにかく型に厳しいことでも知られる(型に厳しいと言うよりは、少し特殊な場合を除いて部分型付けされていない、というだけのことなのだが)。

今回は、式において{ break v; }{ return v; }の型はどういう型になるのかについて書いていきたい。

Rustにおける部分型付け

まず初めにRustの型付けについておさらいしておこう。部分型付けが積極的に採用されている言語では、int <: floatのようにint型がfloat型の部分型である、といった実装になっている。

しかしRustにおいてはそうではない

たとえばf64型の変数にi32型の値は束縛できない。違う型であるとして、型エラーとなる。ここから、Rustにおいてi32 <: f64ではないということは明らかである。

let f: f64 = 42i32; // WARNING: これは型エラーになる

部分型に慣れている人の中にはRustで一番初めに躓いたのがここ、という人もいるのではないだろうか。

ただし部分型付けがまったくされていないというわけではない。たとえばトレイトオブジェクト型はそのもととなったトレイトを実装した型が部分型となる。すなわち、以下のようなコードは許される。

/// トレイトTraitの定義
trait Trait {}

/// トレイトTraitをi32に実装する
impl Trait for i32 {}


let dyn_t: &dyn Trait = &42i32; // これは `i32 <: dyn Trait` なので許される

つまりトレイトTraiti32型に対して実装されているならばi32 <: dyn Traitである

また、参照におけるライフタイムにも部分型付けされている。たとえば'a'bよりも生存期間が長い場合において、&'b T型の変数には&'a T型の値を束縛しても良い

fn f<'a: 'b, 'b>(ref_ia: &'a i32) {
    let ref_ib: &'b i32 = ref_ia;   // これは `&'a T <: &'b T` なので許される
}

つまり'a'bよりも生存期間が長ければ&'a T <: &'b Tである

Rustにおける式の種類

Rustでは宣言文や代入文などを除きすべてが式であるのだが、中にはbreakが式の結果になったり、returnを使っても良い式がある。

しかし自分は普通の式を含むこれらを分類している資料を見つけることができなかったので、この記事では式を以下の3つに分類することとする。
もちろんこれらはこの記事独自の分類で、ほかの場所では通用しない可能性が高いことは留意しておくべきである。

  • break可能式
  • return可能式

これは文字通り普通の式である。ブロック式、if式、if let式、match式がこれに該当する。

この式内部では、breakreturnによって結果を明示的にする方法はない。必ず;を外す必要がある。結果がない場合その式は()型の値として評価される。

// ブロック式
let a: i32 = {
    42i32   // `;` を付けない式が結果となる。
};

// ちなみに途中式のないブロック式は `{}` を省略することが可能
let mut a = 42i32;

// if式
let b: () = if true {
    a += 1; // `;` を付けることで「途中式」となる
            // 結果のない式は `()` 型の値として評価される
};

break可能式

この式はbreakを使うことの可能な式である。while式、while let式、for式、loop式といった繰り返しを表す式がこれに該当する。

この式内部では結果を示すときに必ずbreakによる結果の明示が必要である。結果の明示がない場合かつ無限ループしない式は()型の値として評価される*1

let mut i = 0i32;

// while式
// 無限ループしない式は `()` 型の値として評価される
let a: () = while i < 10 {
    i += 1;
};

// for式
let b: i32 = for i in 0..10 {
    if 3 < i {
        break i;    // `break i;` と書いたことで結果の型は `i32` 型である
    }
};

i = 0i32;

// loop式
// WARNING: この式は無限ループするので注意
let c = loop {
    i += 1;
};

return可能式

この式はreturnを使うことの可能な式である。関数/クロージャの定義で使われる。

この式内部ではreturnを使うことが可能であるが、普通の式と同様に;をつけない場合でもそれが結果となる。式の途中で結果を示す場合にreturnによる結果の明示が必要となる。

特に関数を定義する場合、基本的に引数の型は明示するべきだし、何らかの戻り値があることを期待している場合、型推論を使わず戻り値の型は明示するべきである。
戻り値の型を明示した場合、絶対にその関数の結果は明示した型の値でなければならない*2

/// `return` を使わない関数
fn f(x: i32) -> i32 {
    x + 1i32    // `;` を付けない式が結果となる
}

/// `return` を使った関数
fn fizzbuzz(x: i32) -> String {
    if x % 15 == 0 {
        return "FizzBuzz".to_string();  // `return` を使うと途中で結果を返せる
    } else if x % 3 == 0 {
        return "Fizz".to_string();
    } else if x % 5 == 0 {
        return "Buzz".to_string();
    } else {
        return x.to_string();
    }
}

// クロージャ
let c = |x| {
    x + 1i32
};

// 途中式のないクロージャは `{}` を省略することが可能
let c = |x| x + 1i32;

// もちろんクロージャの場合も `return` が使える
let c = |x| {
    return x + 1i32;
};


以降、この記事において「式」とは常に「break可能でもreturn可能でもない式」のことを示すこととする。

式における{ break v; }{ return v; }の型

さてここからが本題である。式において{ break v; }, { return v; }はどういう型として扱われるのか?

結論としてはどの型であっても良いのだが、それだけだとスッキリしないだろう。そこで、もう一つRustにおいて部分型付けされることが許される場合を書いていく。

!

Rustには戻り値の型としてしか指定できないが確かに型としては存在する隠された型がある。その型とは!である。

!型は特にプログラムにおいて重要な役割を果たす。その役割とは、以下のとおりである。

  • プログラムを中断できる
  • 永久にループできる

型に精通している人、もしくはこの記事を読んだことのある人はお気づきかもしれないが、Rustの!型とは型理論における⊥型*3のことである

Rust、というかほぼすべてのプログラミング言語では、理論上すべての型の部分型として⊥型が存在している。そうしないと中断できるプログラムおよび永久に終了しないプログラムを作ることはできないはずである。

したがって、Rustにおいては! <: i32であるし、! <: &strであるし、! <: Result<(), Box<dyn Error>>である。
すなわち、任意の型Tに対して! <: Tであると言える。

演習問題

以上を理解すると、以下の式の型がどうなるか理解できるだろう。これは問題形式としよう。

fn f(x: Option<i32>) -> String {
    // (1) `v1` の型は何型か?
    let v1 = match x {
        Some(v) => {
            return v.to_string();
        },
        _ => 0i32,
    };
    v1.to_string()
}

// (2) `v2` の型は正しいか?
let v2: usize = loop {};

loop {
    // (3) `v3` において `i32 <: f64` は正しいか?
    let v3: f64 = {
        break 42i32;
    };
};

問題 解答欄
(1) v1の型は何型か?
(2) v2の型は正しいか?
○  ×
(3) v3においてi32 <: f64は正しいか?
○  ×

(1)
(2)
(3)

結論

つまりRustの(break/return可能でない)式において{ break v; }, { return v; }というのはプログラム(厳密にはその式内部での計算)を中断したということになるので、型としては!型となる。
Rustの!型は型理論における⊥型であるためすべての型の部分型であり、ゆえにどの型の変数に束縛しても型エラーにならないのである。

*1:無限ループになる式に関しても一応型はある。詳細は後述

*2:ちなみに、これは変数を束縛するときやクロージャの定義で型を明示した場合でも同様であるが、変数やクロージャの場合は型を明示することは少ない

*3:ボトム型とも言う