- 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` なので許される
つまりトレイトTrait
がi32
型に対して実装されているならば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
式がこれに該当する。
この式内部では、break
やreturn
によって結果を明示的にする方法はない。必ず;
を外す必要がある。結果がない場合その式は()
型の値として評価される。
// ブロック式 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の!
型は型理論における⊥型であるためすべての型の部分型であり、ゆえにどの型の変数に束縛しても型エラーにならないのである。