trifle

技術メモ

すごい if let たのしく使おう

最近, Rust で Cコンパイラを書き始めました.

github.com

コンパイラといっても, これはまだ四則演算と値の比較ができる程度です.
今はそんなに手をつけられなさそうですが, 夏休みになったら一気に進捗を高めたいです.

ところで, このコンパイラを書くにあたって, if let 構文というのを生まれて初めて取り入れました. これ, とても便利だと思ったので, Rust で if let がどのように使われているのかを RFC を読んだりしてちょっと調べてみました.


if let

まず, if let はどういう時に使えると嬉しいのでしょうか.
Rust ではOption型(値が無いことも含めて値として扱う型)やResult型(例外を含めた結果を扱う型, Promise に似ている)などが当たり前のように登場します. 自分で使いこなせれば便利ですし, 自分では使いたくなくても既定のメソッドの返り値の型が Option や Result になっていることが非常に頻繁にあります.
一つ例を考えてみましょう.
char 型のある文字が n 進数の数値を表している場合(すなわちn=10なら '0'から'9' のいずれかの場合), その文字を u32 型(unsigned な 32bit 整数)に変換するメソッド .to_digit(n) というものがありますが(コンパイラで文字から数値を読み取る時に便利ですね!), これは Option 型を返します. というのも, '0'から'9' のどれでもない場合, 変換すべき数が無いからです.
この .to_digit(n) を使って, 『文字を読み取って, もし数であれば加え, そうでなければ何もしない』という処理を書きたいとします. 普通にパターンマッチを使うとこんな感じです.

match c.to_digit(10) {
  Some(n) => {
    hoge += n;
  }
  None => {}
}      

ここで気をつけなければいけないのは, None => {} の部分は省略できないということです. パターンが尽くされていない場合, コンパイルエラーになってしまうからです.
一方, if let を使うと...

if let Some(n) = c.to_digit(10) {
  hoge += n;
}

これだけです. すごい!!!!!!!

  • ネストが一段浅くなって見やすい
  • None の場合の記述が不要な時, 書かなくていい

というメリットが分かるかと思います. たったこれだけ?と思うかもしれませんが, パターンマッチが二重にネストしたコードとかを試しに書いてみるとこの簡潔さがすぐに気に入るかと思います.
この if let は Swift がおそらく発祥であり, RFC にもそのような旨が書かれています.

rust-lang.github.io

書かれていることとしては,

  • パターンマッチにおける None => {}_ => {} のような, 「興味の無いパターンに対しては何もしない」という部分を, if let では省略できる
  • elseif let を繋げて, else if let とすることもできる(その場合, else の後でパターンマッチを行うのと同じ効果が得られる)
  • if let の後で else if を連ねると, else 側に対応する型のパターンマッチにおいてガードを付けるのと同じ効果が得られる
  • 全てのパターンマッチを if let で代替できるかどうかは不明


while let

さて, if let の亜種として while let が登場します. while let は, パターンマッチをループで回し, もし特定の型になった時だけ break する, みたいな処理を綺麗に書けるシンタックスで, これも Swift 発祥のようです.

rust-lang.github.io

これは自分ではまだ使ったことが無いので, 公式ドキュメントにある良い例を借ります.

// `Option<i32>`の`optional`を作成
let mut optional = Some(0);

// 変数の照合を繰り返し行う。
loop {
  match optional {
    // もし`optional`のデストラクトに成功した場合、値に応じて処理を分岐
    Some(i) => {
      if i > 9 {
        println!("Greater than 9, quit!");
        optional = None;
      } else {
        println!("`i` is `{:?}`. Try again.", i);
        optional = Some(i + 1);
      }
      // ^ 3つものインデントが必要。
    },
    // デストラクトに失敗した場合、ループを脱出
    _ => { break; }
    // どうしてこんな行を書く必要が?もっと良い方法があるはずです!
  }
}

optional という変数が Some(1), Some(2) とインクリメントされて, Some(10) になったら次は None になってループが終了するという例ですが, 確かにコメントにある通り _ => { break; } というのはなんか見てて気持ち悪いんですよね. 何かしら while の条件の部分を設定して, while で書けるようにしたい. そういうモチベーションから, while let が生み出されました.

fn main() {
  // `Option<i32>`の`optional`を作成
  let mut optional = Some(0);

  // これは次のように読める。「`let`が`optional`を`Some(i)`にデストラクトしている間は
  // ブロック内(`{}`)を評価せよ。さもなくば`break`せよ。」
  while let Some(i) = optional {
    if i > 9 {
      println!("Greater than 9, quit!");
      optional = None;
    } else {
      println!("`i` is `{:?}`. Try again.", i);
      optional = Some(i + 1);
    }
    // ^ インデントが少なく、デストラクト失敗時の処理を追加で書く必要がない。
  }
  // ^ `if let`の場合は`else`/`else if`句が一つ余分にあったが、
  // `while let`では必要が無い。
}}

if let ほど使用機会は無いかもしれませんが, こちらも書けると気持ち良さそうです.


if let や while let で複式パターンが可能になった(RFC2175)

パターンマッチの長所の一つとして

enum Foo {
  A,
  B,
  C,
  D,
}

fn bar(x: Foo) {
  match x {
    Foo::A | Foo::B => println!("AまたはB"),
    _ => {}
  }
}

この Foo::A | Foo::B のように, 複数のパターンを並べることができる点があります. 普通に if で条件節を並べるよりも簡素ですね.
このような, 複数の型を並べる記法が, if letwhile let にもサポートされるようになりました.

rust-lang.github.io

よって, 先ほどの例は

fn bar(x: Foo) {
  if let Foo::A | Foo::B = x {
    println!("AまたはB");
  }
}

このように書けます.


パターンマッチのガードとして if let が使えるようになるかもしれない(マッチガード)(RFC2294)

2019年6月時点でまだ実装されていない機能です

ガードというのは, パターンマッチの中でさらに制約を絞る時に使うもので, 例えば

match x {
  0 => println!("zero"),
  1 => println!("one"),
  n if is_prime(n) => println!("prime number {}", n),
  n => println!("composite number {}", n)
}

if is_prime(n) に当たるものがそうですが, ここに通常の if だけでなく if let も入れられるようになります.

rust-lang.github.io

この RFC の中に登場する例自体がかなり複雑で, 玄人向け機能という感じが...w
一つ紹介すると

match ui.wait_event() {
  KeyPress(mod_, key, datum) if let Some(action) = intercept(mod_, key) => act(action, datum),
  ev => accept!(ev),
}

ui.wait_event() というのが, あるイベントが発生するのを待つメソッドで, イベントが発生する前にキーの入力を検知した場合の型 KeyPress と, 予定通りのイベントの型 ev でパターンマッチが行われています. そして, KeyPress の場合, intercept(mod_, key) というメソッドで中断して, その間にもしなにか action があればそれを行うという仕組みです. つまり, ガード if let Some(action) = intercept(mod_, key) の部分は全く別のパターンマッチを表現しているという, これが「マッチガード」です.
この例のように, ユーザーインターフェース系の処理は人間がどう操作するかで色々パターンが分岐してしまうので, そういう処理を(あえて) Rust で書きたい人は, こういうシンタックスがあるとありがたそうです.


if let や while let を && で複数連鎖できるようになるかもしれない(RFC2497)

2019年6月時点でまだ実装されていない機能です

これはかなり画期的です.

rust-lang.github.io

例えば, これまで

if let A(x) = foo() {
  if let B(y) = bar() {
    computation_with(x, y)
  }
}

と書かざるを得なかったのが,

if let A(x) = foo()
  && let B(y) = bar()
{
  computation_with(x, y)
}

と書けるようになり, ネストを下げることができます.
いやいや, 従来の記法でも

if let (A(x), B(y)) = (foo(), bar()) {
  computation_with(x, y)
}

こう書けばネスト地獄にならないやん, と思われるかもしれません. でも, これは, 前の2つの例とは明らかに意味が違うのです.
前2つは, foo()A 型であるかを判定し, もしそうでなければその後の処理を打ち切ります. 一方で, 最後の例では, foo()A 型であるかと bar()B 型であるかを同時に判断します. もし foo()A 型でないにもかかわらず bar() が異様に重い処理だった場合, パフォーマンスの差は明らかですよね.


このような if let ... && ... 構文ですが, RFC の文章が非常に長くなっていることからも分かる通り, 導入にあたって議論は紛糾したようです. とりわけ, if let ... && ... を許すなら if let ... || ... はどうなのか? 他の演算子は入れられるのか? と, さらなる論点に繋がっていってしまう部分が問題として取り上げられています.
個人的には, この RFC の中においても, そもそも発祥の Swift ではif letの連鎖はどう書けるのか, というのが再び取り上げられているのが面白いな〜と思いました.



if let, while let は先進的なシンタックスであるだけに, 今後もさらなる機能拡張が起こるかもしれません. 楽しみですね.