Rust入門者とif letとの出会い
if let文は公式チュートリアルでは、パターンマッチングの章で初登場します。入門者はmatch
によるパターンマッチングを学んだ後で、「値が一つのパターンにマッチした時の動作だけを書きたいなら、match
の代わりにif let
という記法を使えば短く書けるよ」と習うことになります。
//https://doc.rust-jp.rs/book-ja/ch06-03-if-let.htmlから引用
// 値がSome(3)の時だけコードを実行するmatch:
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
// if letを使うと短く書くことができる:
if let Some(3) = some_u8_value {
println!("three");
}
この説明を見て「なるほど、パターンマッチに網羅性が要らない時はif let
を使えばいいんだな」とストンと腑に落ちる人は、何の問題もありません。次に進みましょう。そうではなく、「なんだこの構文?パターンマッチの話をしてたのにどうして急にif
とlet
が......?」と立ち止まってしまった(私のような)人達1に向けてこの記事を書きました。
if let
は既存のif
とlet
を利用したテクニックではない
まず最初にはっきりさせておくべきは、if let
記法が「既存のif
とlet
を利用した頭の良いテクニック」ではなく、if let
という新たなキーワードを使った、れっきとした別の記法であるということです。実際、導入時には、if_let
やiflet
といった書き方も検討された形跡があります。else if
やextern crate
もそうですが、Rustは既存のキーワードの組み合わせで新たなキーワードを作ることに抵抗がないようですね。
とはいえ、わざわざif
とlet
という既存のキーワードを組み合わせて作る訳ですから、この記法にはif
文の気持ちとlet
文の気持ちが入っていることになります。ではどういう風にif let
記法を読めば腑に落ちるのかを次に見ていきます。
if let記法の構造
改めてif let記法の構造を書き下しましょう。
一般的な形は以下のようになります。実はelse
節も使えます。
ここで、いったん色々飲みこんでlet hoge = piyo
の部分がtrue/falseを返すboolであると仮定してみることにしましょう(この仮定自体は誤りですが、文の「お気持ち」を掴むには有効なので)。すると、文構造自体は普通のif
文そのものと気付くと思います。このように考えると、上の文の意味するところは
「let hoge = piyo
という操作の真偽をifで判定して、{}内の動作を切り替える」
と捉えてよさそうです。とすると次の問題はlet hoge = piyo
は何をしているのか?です。
letの真の意味
let hoge = piyo
が「判定」に使われるというのは、奇妙に感じられるかもしれません。というのも、多くの入門者は「let hoge = piyo」の意味するところを「値piyoを変数hogeに代入する」と習ったはずだからです(公式チュートリアルもそう説明している)。しかし実は、公式チュートリアルの後半、18章で種明かしされているように、letの「真の姿」は以下のような形をしているのです。
let PATTERN = EXPRESSION;
// EXPRESSIONとは、何らかの値を返す「式」のこと
// PATTERNとは、値がマッチするか否かに用いられる「パターン」のこと
左辺が「変数」のようなものではなく、「パターン(PATTERN)」であることに着目してください。letの真の動作とは「右辺の式(EXPRESSION)が返す値を左辺のパターン(PATTERN)に当てはめ、PATTERNに従って変数に値を束縛する」というものだったのです。たとえば、let x = 5;
のような単純な代入が実際に行っている内容を大仰に書き下すと、「右辺の式が返す値"5"を左辺のパターン"x"に当てはめて、"x"に"5"を束縛する」という動作をしていることになります。
このような動作の恩恵は、より柔軟な代入が可能となる点です。下の例を見てください。右辺のタプルを分配して左辺に代入する、といった芸当が可能になっていることが分かります。
let (x, y) = (3, 4);
assert_eq!(x, 3); // xに3が代入されている
もし右辺の値が左辺のパターンに当てはまらなかったら?
以下のコードがその例です。左辺のパターンは2要素のタプル(x, y)ですが、右辺の式として3要素のタプルを与えました。これは明らかにマッチせず、「mismatched types」であるとコンパイル時にエラーで怒られることになります。
let (x, y) = (3, 4, 5); // コンパイルエラー: mismatched types
では、次の例はどうでしょうか。
左辺のパターンとしてSome(x)
、右辺の式としてSome(3)
を与えました。
let Some(x) = Some(3); // コンパイルエラー: refutable pattern
これは「x = 3」としてマッチしても良い、と思うかもしれません。が、実際にはこれもコンパイルエラーになります。
エラーメッセージは「refutable pattern in local binding: None
not covered」。直訳すれば「変数の束縛に論駁可能なパターンが使われています。None
の場合がカバーされていません」です。どういうことかというと、右辺にはOption
型の値が与えられているので、Some(x)
またはNone
の2つのパターンが考えられるのに対し、左辺はSome(x)
のパターンしか考慮されていないので、 パターンに合致しない可能性がある(= 論駁可能)ということです。let
文は、論駁不可能なパターンしか受け付けません。この制限によって、let
文は意味的には結局「代入」を表す文として動作することになります。
(※ 上のサンプルコード上では右辺がNone
になる可能性はありませんが、パターン上では論駁可能なので不可)
if letの登場: 論駁可能なパターンへの対応
前項の話をまとめると、以下のようになります。
- 本来
let
にはパターン判定の意味がある - パターン判定には「論駁可能性」が存在し、「論駁可能」な場合はプログラム実行中にパターンが一致したりしなかったりする
- 実際の
let
文は、論駁不可能なパターンしかコンパイルを許さないので、実行中に「パターン不一致」の判定を下す機会はない
こう並べられてみると、「let
キーワードを使って、実行中のパターン不一致に対応した構文があっても良いのではないか?」と思うのは自然な発想のように思います。それがif let
文です。
以下のコードはコンパイル可能です。
if let Some(x) = Some(3) {
assert_eq!(x, 3);
}
「もしlet
によるパターンマッチングが成功するのなら、xに3を束縛して{ }スコープ内の命令を実行する」
今なら自然に読めてきませんか?
おまけ: 初心者が混乱する理由
正直、if let
文については公式チュートリアルの説明が少々分かりにくいな、と思っています。説明したとおり、if let
文の本質はパターンマッチング成否判定と変数束縛を同時に行うことが出来る点にあるわけですが、最初に説明した公式チュートリアルはの説明例では変数束縛をしていないんですよね。こういう例だと時は「普通にif
文を使えば良いのでは?」と思うでしょうし、実際if
文を使います。チュートリアルの説明順序上の都合があるんだと思いますが......。
2022/11/10追記: let-else文
Rust ver. 1.65.0にて、長らく待望の「let-else文」が安定化されました。
https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html
これにより「実行中のパターン不一致に対応したlet
系構文」がもう一つ増えたことになります。
論駁可能なパターンに対して、マッチする場合のみ変数を束縛する、という動作は両者に共通しています。違うのは、if let
が「新たに作ったブロック」内で変数を束縛するのに対し、let else
は「現在のブロック」で束縛する、という点です。
// 変数のxへの束縛範囲
// if-let文
if let Some(x) = Some(3) {
// ------↓ここから束縛↓------
assert_eq!(x, 3);
// ------↑ここまで↑------
} else {}
// let-else文
let Some(x) = Some(4) else {
return
};
// ------↓ここから束縛↓------
assert_eq!(x, 4);
束縛した変数を長く使いたいならlet-else
が便利です。
参考サイト
- 論駁可能性について: https://doc.rust-jp.rs/book-ja/ch18-01-all-the-places-for-patterns.html
-
if let
導入に関する議論: https://github.com/rust-lang/rfcs/pull/160