100
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustの「if let」とは何なのか?

Last updated at Posted at 2022-06-11

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を使えばいいんだな」とストンと腑に落ちる人は、何の問題もありません。次に進みましょう。そうではなく、「なんだこの構文?パターンマッチの話をしてたのにどうして急にifletが......?」と立ち止まってしまった(私のような)人達1に向けてこの記事を書きました。

if letは既存のifletを利用したテクニックではない

まず最初にはっきりさせておくべきは、if let記法が「既存のifletを利用した頭の良いテクニック」ではなく、if letという新たなキーワードを使った、れっきとした別の記法であるということです。実際、導入時には、if_letifletといった書き方も検討された形跡があります。else ifextern crateもそうですが、Rustは既存のキーワードの組み合わせで新たなキーワードを作ることに抵抗がないようですね。

とはいえ、わざわざifletという既存のキーワードを組み合わせて作る訳ですから、この記法にはif文の気持ちとlet文の気持ちが入っていることになります。ではどういう風にif let記法を読めば腑に落ちるのかを次に見ていきます。

if let記法の構造

改めてif let記法の構造を書き下しましょう。
一般的な形は以下のようになります。実はelse節も使えます。
if letの構造
ここで、いったん色々飲みこんで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が便利です。

参考サイト

  1. e.g.) a, b, c

100
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
100
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?