Syntax and Semantics
前回に続いて、Rustのoverview的なチュートリアル”Dining Philosophers”を扱うつもりでしたが、 “Syntax and Semantics”章も読み始め、そちらの記事を書いてしまったので、 “Dining Philosophers”は次回に回します。
というわけで、Rustの syntaxとsemanticsを解説したページ を読んでいきます。
Variable Bindings
variable bindingについての節です。ややこしいところではないので簡単に読み進めます。
variable bindingの宣言は、
let x = 5;のようにlet statementをつかいます。letが受け入れるのは’pattern’であって、例えば
let (x, y) = (1, 2);のようにできます。このときx = 1, y = 2となります。
‘pattern’については後の節で詳しく説明されるそうです。
上の例では型を明示していませんでした。明示するには
let x: i32 = 5;のようにします。
Rustのvariable bindingはデフォルトでimmutableなので、mutableにするには
let mut x = 5;のようにmut指定します。不必要なmut指定があると、コンパイラが警告してくれるようです。
最後に、Rustでは未初期化のvariable bindingはコンパイルエラーになります。例えば
let x: i32;
println!("The value of x is: {}", x)はエラーです。
以上でvariable bindingが簡単に説明されました。 次に進みましょう。
Functions
関数ですね。ここも難しい節ではないですが、Rustで重要な’expressions’と’statements’の 違いが浮き彫りになってくるようです。
関数はfnキーワードで宣言します。引数をとるには、関数名のあとの丸括弧()に名前と型を書きます。
値を返すには->のあとに型を書きます。
fn sum(x: int32, y: int32) -> i32 {
x + y
}といった具合です。 Rustには型推論がありますが、引数の型は明示する必要があります。 理由をはっきりと述べるのは難しいですが、関数がどんな型をうけとるかくらいはユーザーが 予めわかっていたほうがいい、というの(が理由の一つなこと)はなんとなく理解できます。
注目すべきは、x + yにセミコロン;がついていないことです。
ここに、Rustがexpression-based languageであることが読み取れます。
Expressions vs. Statements
Rustはexpession-based languageです。statementには二種類あり、 それ以外はすべてexpressionです。はじめにまとめておくと、次のようになるようです。
- expression
- statement
- declaration statement
- expression statement
違いを一言で言うと、expressionは値を返し、statementは返さない、ということらしいです。
したがってstatementであるx + 1;は値を返さず、
fn add_one(x: i32) -> i32 {
x + 1;
}はコンパイルエラーになります。
まずはdeclaration statementについて、ドキュメントに載っている例を挙げてみます。
let x = let y = 5やlet x = (let y = 5);はエラー- 宣言済みのvariable bindingへ再代入する、
rust let mut y = 5; let x = (y = 6);はエラーではないが、xに空tuple()がassignされる。 丸括弧()は必要ないので(コンパイル時に警告が出る)let x = y = 6とできるが、 このときもxには空tupleがassignされる。rust let mut x; x = y = 6;も同様。
まず1.について。let statementはstatementです。よって値を返さないので、 他のlet statementに渡して代入することはできません。
次に2.について。Rustではデータの所有権(ownership)を持つvariable bindingは一つのみです。
よってlet x = (y = 6)は、’6’のownershipをx, y両方に与えるのではなく
yのみに与え、xにはexpressionであるy = 6が返した()がassignされる、という
仕組みのようです。
なるほど、ややこしいですね。x = y = 6をエラーにせず、空tupleを代入する理由は何か
あるのでしょうか?
今のところはよくわかりませんが、ともかくownershipの概念が重要のようです。
statementにはもう一種類あります。expresion statementです。
これはexpressionをstatementに変換するもので、Rustコードのほとんどの行は
statementでできている、と書いてありました。
つまりexpressionをセミコロン;で区切ることでstatementにし、statementどうしを
区別している、というわけです。
これはC++などの言語でみられる文法ですね。
そしてstatement以外がexpressionなのでした。 上で述べたように、Rustコードのほとんどはstatementで、例外的なexpressionが、
fn add_one(x: i32) -> i32 {
x + 1
}のx + 1という行です。もしx + 1;とすると、これはstatementになり、
この関数は()を返すという意味になるそうです。
そういえば私はRubyをすこし触ったことがあるのですが、
Rubyでは関数の最後に評価された式の値が戻り値になるのでした。
その話をきいたときは、Rubyは関数型の特徴ももっており、(引数でパラメータづけられた)関数は
その戻り値と対応しているからそういう仕様なのか(あるいは単に利便性のためか)と考えていました。
手続き型はreturn文という手続きを踏んで値を返し、関数型は関数と戻り値が等価なので
戻り値を書くのみの文法にしている、という対応付けができそうだと思っています。
Early returns
そのreturn文ですが、マルチパラダイムなRustには当然用意されています。
fn foo(x: i32) -> i32 {
return x;
x + 1 // we never run this line
}このように書くとreturnで関数を抜けるという、慣れ親しんだ動作をするようです。
Diverging functions
Rustには、’diverging functions’という特殊な関数があるそうです。 まず、syntaxは以下のとおりです。
fn diverges() -> ! {
panic!("This function never returns!");
}panic!()はmacroの一つで、実行されるとそのスレッドがクラッシュします。
この関数を呼ぶとクラッシュして値を返さないので、戻り値に!を書く、
これが’diverging functions’というわけです。
例外を投げるということでしょうね。catch方法はまだわかりませんが。
panic!()が実行されると、
thread ‘<main>’ panicked at ‘This function never returns!’, hello.rs:2のような出力を得ます。panic!()した、という情報だけでいいなら十分ですが、
RUST_BACKTRACEという環境変数を設定すると、バックトレース情報がいっしょに出力されます。
$ RUST_BACKTRACE=1 ./diverges
thread '<main>' panicked at 'This function never returns!', hello.rs:2
stack backtrace:
1: 0x7f402773a829 - sys::backtrace::write::h0942de78b6c02817K8r
2: 0x7f402773d7fc - panicking::on_panic::h3f23f9d0b5f4c91bu9w
3: 0x7f402773960e - rt::unwind::begin_unwind_inner::h2844b8c5e81e79558Bw
4: 0x7f4027738893 - rt::unwind::begin_unwind::h4375279447423903650
5: 0x7f4027738809 - diverges::h2266b4c4b850236beaa
6: 0x7f40277389e5 - main::h19bb1149c2f00ecfBaa
7: 0x7f402773f514 - rt::unwind::try::try_fn::h13186883479104382231
8: 0x7f402773d1d8 - __rust_try
9: 0x7f402773f201 - rt::lang_start::ha172a3ce74bb453aK5w
10: 0x7f4027738a19 - main
11: 0x7f402694ab44 - __libc_start_main
12: 0x7f40277386c8 - <unknown>
13: 0x0 - <unknown>main()から呼んだdiverges()でpanic!()していることが読み取れます。
この環境変数はcargo runにも使えるそうです。
奇妙なことに、diverging functionはどんな型にも代入できるようです。
let x: i32 = diverges();
let x: String = diverges();これどういう意味があるんでしょう?
Function pointers
関数をvariable bindingに代入することもできます。
fn plus_one(i: i32) -> i32 {
i + 1
}
let f: fn(i32) -> i32 = plus_one; // 明示的な型指定
let f = plus_one; // 型推論
let six = f(5);簡単ですね。
今回はこれで終わりです。
すこしRustを書いていて感じたことに、コンパイラによる警告が優秀というのがあります。
不必要なvariable bindingの宣言やmut指定はもちろんのこと、
例えば let mut x = 5; x = 6;のように、代入した値を使わずすぐ代入しなおしているコードも
拾って警告してくれました。
最適化したコードを書く助けになりそうです。