Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Rust における直観に反するように見えるコード

ちょっとおもしろいツイートを見つけたので、メモがてら書きます。

次のようなコードはコンパイルが通ってしまいます。

fn main() {
    let a;
    let b;
    let z = (a = 10) == (b = 20);
    assert_eq!(z, true);
}

もちろん、a には 10 が代入されており、b には 20 が代入されており、それを比較する z は当然 10 == 20 の式になって false …と直感的には思えてしまうのですが、厳密には異なります。

C や Java などを用いていると、a = 10 の評価後、得られる値は 10 なので、10 と(b も同様に考えて) 20 が比較されていると思われます。これらの言語では、代入は文だからです。

class Main {
    public static void main(String[ ] args) {
        int a;
        int b;
        boolean z = (a = 10) == (b = 20);
        System.out.println(z);
    }
}

このサンプルコードは Java ですが、出力結果は false になります。10 == 20 が成立するからです。

一方で Rust においてはこの方も説明されている通り、a = 10b = 20 は式であり、この式の評価結果の値は ()(unit 型*1)なのです。この値同士を比べる演算が行われるため、() == () となって ztrue になるという話のようです。

これは新しい変数 c を作るとコンパイルエラーから知ることができます。

fn main() {
    let c;
    println!("c?: {}", (c = 50));
}

これをコンパイルすると、

error[E0277]: `()` doesn't implement `std::fmt::Display`
 --> src/main.rs:8:24
  |
8 |     println!("c?: {}", (c = 50));
  |                        ^^^^^^^^ `()` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `()`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`

error: aborting due to previous error

このエラーの概要は、() 型に対して std::fmt::Display が実装されていないので、標準出力には用いることができませんよという話です。c = 50 という式はやはり、() として判別されているようですね。

もちろん、a, b には値は代入済みのようで、下記のようにさらに書き直しを行うとそれを確かめることができました。

fn main() {
    let a;
    let b;
    let z = (a = 10) == (b = 20);
    let another_z = a == b;
    assert_eq!(z, true);
    assert_eq!(another_z, false);
}

これは通ります。

しかし、うーん、ちょっと直観に反しますね。

*1:Rust では要素 0 個の Tuple は Unit 型として判定されます。() は要素0個のタプルの値である一方で、それ自身の型は Unit 型でもあります。