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文を使えば?」と思った方は少なくないでしょう。実際、これはif文を使うべき場面です(チュートリアルの説明順序上の都合があるんだと思いますが......)。
じゃあどういう時にif letを使うのか?たとえば、下のような時です。
// ValueがNoneでなければprintする関数
fn print_if_exist(value: Option<u8>) {
if let Some(x) = value {
println!("Value {} exists!", x);
}
}
さっきの例と違って、Some(x)のように新しい変数xが左辺に含まれていて、そのxを{}内で利用していることに注目してください。言語化すれば「パターンマッチングと変数束縛を同時に行いたい時」ということになります。今はまだこの言葉の意味が分からなくても大丈夫です。以下順を追ってこの構文が一体何なのかを説明します。
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文が「判定」に使われるというのは奇妙に感じられるかもしれません。たとえば変数xに値3を代入するような操作に成功も失敗もないだろう、と。
しかし実はRustのletは、これら単純な例よりもずっと柔軟な動作をすることが出来ます。
いくつか実例を見てみましょう。
例1. 右辺のタプルを分配して左辺に代入できます
let (x, (y, z)) = (10, (20, 30));
assert_eq!(x, 10); // xに10が代入されている
assert_eq!(y, 20); // xに20が代入されている
assert_eq!(z, 30); // xに30が代入されている
例2. _による任意パターンが使えます
let (x, _) = (10, (20, 30));
let (x, _) = (10, 20);
assert_eq!(x, 10); // 上記どちらでもxに10が代入される
例3. 構造体のフィールド抽出ができます
struct Pt { x: i32, y: i32 }
let Pt { x, y } = Pt { x: 5, y: 7 };
assert_eq!(x, 5); // xに5が代入されている
assert_eq!(y, 7); // xに7が代入されている
などなど。
こうなってくると、左辺は変数xのように「何でも入る単なる箱」ではなく、一種の「規則を持った変数」、すなわち「パターン」であると言えます。実際、これらの左辺の形は、公式チュートリアルのmatchの項目で説明されている「パターン記法」そのものです。
公式チュートリアルの後半、18章では、letの「真の姿」は以下のような形であると種明かししています。
let PATTERN = EXPRESSION;
// EXPRESSIONとは、何らかの値を返す「式」のこと
// PATTERNとは、値がマッチするか否かに用いられる「パターン」のこと
つまりletの真の動作とは「右辺の式が返す値を左辺のパターンに当てはめ、パターンに従って変数に値を束縛する」というものだったのです。たとえば、let x = 5;のような単純な代入が実際に行っている内容を大仰に書き下すと、「右辺の式が返す値"5"を左辺のパターン"x"に当てはめて、"x"に"5"を束縛する」という動作をしていることになります。
パターン不一致の可能性
let文の左辺がパターン記法で記述されるということは、右辺の値が左辺のパターンに当てはまらない可能性が生じる、ということです。
以下のコードがその例です。左辺のパターンは2要素のタプル(x, y)ですが、右辺の式として3要素のタプルを与えました。これは絶対にマッチせず、「mismatched types」であるとコンパイル時にエラーで怒られることになります。どうあがいてもコンパイルできません。
let (x, y) = (3, 4, 5); // コンパイルエラー: mismatched types
では、次の例はどうでしょうか。
左辺のパターンとしてSome(x)、右辺の式としてOption<u8>型の変数valueを与えました。
// 受け取ったvalueの中身をprintしたかった関数
fn print_value(value: Option<u8>) {
let Some(x) = value; // コンパイルエラー: refutable pattern
println!("value is {}.", x);
}
たとえばvalueがSome(3)の時に「x = 3」のようにマッチしても良い、と思うかもしれません。が、実際にはコンパイルエラーになります。
なぜか?
右辺の形はSome(x)またはNoneの2つのパターンが考えられるのに対し、左辺はSome(x)のパターンしか考慮されていないためです。このように、実行時に右辺が左辺のパターンに合致しない可能性がある状態を「論駁可能(refutable)」と呼びます。
エラーメッセージは「refutable pattern in local binding: None not covered」。直訳すれば「変数の束縛に論駁可能なパターンが使われています。Noneの場合がカバーされていません」。valueにNoneが与えられた場合にこのプログラムは何をすべきかが定義されていないため、コンパイル出来ないというわけです。
let文は、論駁不可能だとコンパイル時に保証できるパターンしか受け付けません。この制限によって、let文は意味的には結局「代入」を表す文として動作することになります。
if letの登場: 論駁可能なパターンへの対応
前項までの話をまとめると、以下のようになります。
- 本来
letにはパターンマッチの意味がある - パターンマッチには「論駁可能性」が存在し、「論駁可能」な場合はプログラム実行中にパターンが一致したりしなかったりする
- 実際の
let文は、論駁不可能なパターンしかコンパイルを許さないので、実行中に「パターン不一致」の判定を下す機会はない
こう並べられてみると、「letキーワードを使って、実行中のパターン不一致に対応した構文があっても良いのではないか?」と思うのは自然な発想です。それがif let文です。
| 用語 | 意味 | 対応文法 |
|---|---|---|
| 論駁不可能(irrefutable) | 実行時に必ずパターン合致する |
let文 |
| 論駁可能(refutable) | 実行時にパターン合致しないかもしれない |
if let文 |
以下のコードはコンパイル可能です。
// ValueがNoneでなければprintする関数
fn print_if_exist(value: Option<u8>) {
if let Some(x) = value {
println!("Value {} exists!", x);
}
}
「もしvalueの値がSome(x)のパターンに合致するのなら、変数xに対応する値を束縛して{ }スコープ内の命令を実行する」
今なら自然に読めてきませんか?
おまけ: 英語との対応
英語のletは"let A be B"の形で「AをBとする」という意味になるので、
-
let A = B: Aの値をBとする -
if let A = B {}: もしAの値をBとすることが出来るならば.....
と自然に読むことが出来ます。
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