Rust を書きはじめて日が浅い私は「ここにセミコロン書いていいんだっけ?書かなくていいんだっけ?」と混乱することがよくあります。
ちょっと作為的ですが例ですが、たとえばこういうとき。
#[allow(dead_code)]
fn main() {
struct Foo {x: i64} // ◁─┬─ 0個でも1個でもいい
struct Bar {x: i64}; // ◁─┘
struct Baz(i64); // ◁─┬─ 1個でも2個でもいい
struct Qux(i64);; // ◁─┘
const A: i64 = 1; // ◁─┬─ 1個でも2個でもいい
const B: i64 = 1;; // ◁─┘
let _x = 1; // ◁─┬─ 2個だと unnecessary trailing semicolon と怒られる
// let _y = 1;; // ◁─┘
if true {
"hoge"; // ◁─┐
} else { // ├───┬─ どっちか片方だけでもいい
"fuga"; // ◁─┘ │
}; // ◁─────┘
}
基本的にはおおよそ
- それが返したい値なら
;
をつけない -
}
のあとならつけない、そうでなければつける - コンパイラに怒られたらつける
という感じで覚えていますが、それって正しいの? 文法的にどうなっているの? というのをちゃんと理解しようというのがこの記事の趣旨です(チートシート的な内容ではないのでお気をつけください)。
なお簡単のため、マクロについてはいったん忘れるものとします(マクロも含めてきちっと理解して情報をまとめることができず……)。
前提知識: expression と statement
プログラムというのは
- Expression: 式 ……「値」に評価されるコード片(≒変数に代入できるもの) 1
- Statement: 文 …… そうでないもの
の2種類のコード片を組み立てて構成されます。
Rust でいえば、こんな感じ。
2020年6月4日追記
@wataash さんに コメント で AST Explorer というツールを教えていただきました(ありがとうございます)。
手を動かしながら理解したい方には https://astexplorer.net/ をおすすめしたいです。
コードをいじって構文木を眺めながらこの記事と照らし合わせるととても理解できると思います。
これを使うと Rust のコードも AST に分解してドリルダウンできるので理解が進みます(最初に開いたときは対象言語が JavaScript になっているので、上部のメニューから変更してください)。
Rust はさまざまなものが式になるのが特徴の一つで
Rust is primarily an expression language
Statements and expressions - The Rust Reference
といわれるほどです。
たとえば if
。手続き型の色が濃いは言語では if
は式ではなく文であることが多いでしょう。でも Rust では if
は式であり、その評価値を変数に代入できます。
let result = if ok { "success" } else { "failure" };
このようにフローコントロールを含むほとんどが expression であり、expression に expression を積み重ねてできあがるのが Rust のコードです。
Expression の分類
Expression はさらに大きく2つに分類できます。
-
Expression with block: ブロックありの式 …… block expression(後述)・
loop
(for
なども含む)・if
・match
・async
・unsafe
-
Expression without block: ブロックなしの式 …… 上記以外の式(ほとんどの言語でも同様に expression となるようなもの)
- 例: リテラル・変数・演算子の適用・関数呼び出し・フィールドアクセスなど
前者は
- フローコントロールに使われるものが多い
-
{}
がなくてはいけない - その expression が評価されるときに
{}
の評価もされる
といった特徴があります。
たとえばこの if expression の値は2つの block expression のいずれかの値ですね。
一方で {}
を構成要素としながらも expression without block に分類されるものもあります。たとえば closure expression。
この block expression が評価されるのはこの closure expression 自体(closure の「定義」)が評価されるときではなく、呼び出されるときです。
Block expression
さて、expression は強力ですが、単一の expression だけで現実的な処理を書くのは厳しいものがあり、複数の expression を直列に並べてコードを書きたくなると思います。
そこで複数の expression を組み合わせて1つにまとめあげるのが block expression です。関数定義の本体部分もこれですし、if
など前述の expression with block の本体もほとんどこれです2(block expression 自体も block with expression の一種です)。
似た名前のものがたくさん出てきてややこしくなりましたが、関係を整理するとこうなります。
- Expression ⊃ Expression with block ∋ Block expression
Block expression は0個以上の statement と、最後に0個または1個の expression からなります。公式の記法だとこんな感じ(肩の上付き記号は正規表現と同じだと思ってください)。
{
$Statement^* Expression^?$}
Block expression 自体の評価値は、最後の expression の評価値になります。もし expression がなければ(≒statement で終わっていれば)、評価値は ()
です。
……あれ?
Expression をいっぱい並べたい!という話だったのに、いっぱい並べられるのはあくまで statement であって、expression は最後に高々1つしか書けません。ということは、{}
の中にいつもたくさん書いているものは、ほとんど statement だということです。
どういうことでしょうか。
Statement の分類
Block expression の定義により statement の重要性が分かったところで、今度は statement について深堀りしていきましょう。
Statement も大きく分けると下記のように分類されます。
-
Declaration statement: 宣言文
-
Let statement: Let 文 ……
let
による変数宣言 -
Item: アイテム …… 変数以外の定義・宣言(
const
,fn
,struct
など)。実行時ではなくコンパイル時に評価される
-
Let statement: Let 文 ……
- Expression statement: 式文 …… 式のみの文
- 空 statement ……
;
のみの文
2つ目がミソです。Expression なのに statement?
そうです。Expression は末尾に ;
をつけることによって statement になるのです。このおかげで、block expression の中にたくさんの expression を(expression statement の形で)書くことができるようになりました。
さて、ここまでで前提知識は十分です。セミコロンの謎に迫りましょう!
セミコロン ;
の役割
前項で「expression を statement に変換するもの」として ;
を紹介しましたが、Rust ではそれ以外にも ;
が出てくるところがあります3。
ひととおり挙げてみます(文法はいろいろと端折っています)。
# | ; |
文法 | 備考 |
---|---|---|---|
0 | $Expression$ | ||
1 | ✓ | $Expression$;
|
|
2 | ✓ |
let NAME = $Expression$;
|
|
3 | ✓ |
const NAME: TYPE = $Expression$;
|
|
4 | ✓ |
static NAME: TYPE = $Expression$;
|
|
5 | ✓ |
type NAME = TYPE ;
|
|
6 | ✓ |
use ... ;
|
|
7 | ✓ |
extern crate NAME;
|
|
8 |
struct NAME { ... }
|
C-like | |
9 | ✓ |
struct NAME;
|
empty |
10 | ✓ |
struct NAME ( ... );
|
tuple |
11 |
enum NAME { ... }
|
||
12 |
union NAME { ... }
|
||
13 |
fn NAME ( ... ) { ... }
|
||
14 | ✓ |
fn NAME ( ... );
|
trait /extern 内の定義 |
15 | ✓ |
mod NAME;
|
import |
16 |
mod NAME { ... }
|
||
17 |
trait NAME { ... }
|
||
18 |
impl TYPE { ... }
|
||
19 |
extern " ..." { ... }
|
||
20 | ✓ |
; 単体 |
空 statement |
0番は statement ではなく expression です。ブロックから値を返したい場合はこの形である必要があるので、;
をつけてはいけません。逆に statement にしたい(ブロックの途中で使いたい)場合は ;
必須です(……といいつつ例外があるよ、という話をこのあとします)。
末尾が expression になる statement は ;
で終わる取り決めにしたほうが人間にとってもコンパイラにとってもパースしやすいというのは納得ができます。
それ以外でいえば、末尾が {}
があれば ;
不要、なければ ;
必要というふうに決まっているものが多い印象です。ただし注意が必要なのは use
で複数アイテムを取り込むときで、{}
で終わりますが ;
が必要です。
さて、なんとなく ;
要不要の分類は見えてきましたが、まだややこしい問題があります。
それは、;
の必要な個数に曖昧性があるということです。次はその具体的なケースを見ていきましょう。
Item のあとの空 statement ;
上記リスト20番の「;
単体」は文法上はいくらでも書けるのですが、実際には書いてコンパイルすると "unnecessary trailing semicolon" といって WARNING が出ます。
ところが item statement(2〜19番が該当)のときだけは、なぜか1つ余計に ;
をつけることが許されているようです(なぜ?)。冒頭に上げた例の、最初の3組の例がこれに該当します。
なお、これらは let statement や expression statement と違って「関数の外側」に書けるのですが、その場合は ;
を余計につけると怒られます(外側に書けるのは item だけで、;
単体は item ではないので当然です)。
この「余計な」;
は怒られないものの不要なのは間違いないので、付けない方針にするのがよいと思います。
たとえば
#[allow(dead_code)]
fn main() {
struct Foo {x: i64}; // ◁── 1個
struct Bar(i64);; // ◁── 2個
const A: i64 = 1;; // ◁── 2個
}
ではなく
#[allow(dead_code)]
fn main() {
struct Foo {x: i64} // ◁── 0個
struct Bar(i64); // ◁── 1個
const A: i64 = 1; // ◁── 1個
}
とします。
どの statement が ;
0個でどれが1個か覚えておきたければ、
-
use
,let
,const
,static
は;
1個 - あとは
}
で終わっていれば;
0個、そうでなければ1個
と分類できそうです。
より厳密に確認するには
- 同じ文を
{}
の外に書いて怒られるかどうか見る(外では;
を余計につけられないので) -
;
を全部消して怒られるかどうか見る
などの見分け方もあります。
Expression statement における ;
の省略
Expression に ;
をつけて expression statement にするといいましたが、この ;
はある条件のとき省略できます。
それは、対象となる expression が expression with block であるとき(ただしこの場合、expression の評価値は ()
でなくてはいけません)。
なお、この ;
省略ルールが適用されるのは expression statement のときだけで、ほかの let
, const
, static
などのときは適用されません。末尾に ;
をつけるのが必須です。
こちらに関しては、個人的には見た目が C などに近くなって馴染みやすいので、省略するのが良いと思います。
たとえば
#[allow(dead_code)]
fn main() {
if true {
do_something(); // ◁── あり
} else {
do_other_thing(); // ◁── あり
}; // ◁── あり
}
ではなく
#[allow(dead_code)]
fn main() {
if true {
do_something(); // ◁── あり
} else {
do_other_thing(); // ◁── あり
} // ◁── なし
}
とします。
おわりに
Rust における ;
を文法から知ることことで、どういうときに必要でどういうときに不要かが以前よりもすっきりと理解できるようになりました。
つけてもつけなくてもいい場合にどうすべきかは筆者個人的な判断によるものですが、それを含めて最後にフローチャートにまとめてみたいと思います。
参考リンク
-
Introduction - The Rust Reference
- 公式の文法解説
-
Rustの文でセミコロンを省略してよい条件 - 簡潔なQ
- Expression statement のパースについて少し詳しく書かれています
-
Rust においてややこしいのが
return
とbreak
expression でしょうか。評価された瞬間にスコープを抜けてしまうので値を持つ/持たないを考えても意味がない(絶対に使えない)ので。 ↩ -
match
expression だけは違います。これは expression with block に分類され、実際{
}
によってブロックを作りますが、そのブロックは厳密には block expression ではありません。 ↩ -
Statement の文法以外では長さ指定で配列やベクターを作る表記
[0; 128]
にも;
は出てきますが、今回は関係ない(混同することもない)と思うので無視します。 ↩