Panda Noir

JavaScript の限界を究めるブログです。

代入演算子を連結したら直感に反する挙動になった

ことの発端は、変数入れ替えのコードゴルフをしていたときでした。変数入れ替え手法のひとつとしてa^=b;b^=a;a^=b;というものがあるのですが、これを短くしようと躍起になっていたら起きました。

該当のコード

let a = 1, b = 2;
a^=b; b^=a; a^=b;

この実行結果は a=2,b=1 です。このコードを次のように変形してみました。

let a = 1, b = 2;
a^=b^=a^=b;

ちょっと挙動をみやすくするためにカッコを使うとこんな感じです

let a = 1, b = 2;
a^=(b^=(a^=b));

これを実行すると、予測に反して a=0,b=1 となります。もちろん期待した挙動は a=2,b=1 です。

そもそもなぜこんなコードを書いたのか

代入を簡略化しようとしたせいです。

a=b,b=ab=a=b と等価です。そのノリで a^=b;b^=a;b^=a^=b と等価だろうと考えたのです。

いえ、実は このコード自体は完全に等価です。さらにa^=を加えたために問題が起きました。

何が起きたのか

XORだとわかりづらいので加算にします。

// No.1
let a = 1;
a += a;
a += a;
a += a;
a += a;

// No.2
a = 1;
a += (a += (a += (a += a)));

さて、No.1とNo.2、それぞれ a はどうなると思いますか?答えは 165 です。

それぞれわかりやすく書き直してみます。

// No.1
a = a + a;
a = a + a;
a = a + a;
a = a + a;

// No.2
a = a + (a = a + (a = a + (a = a + a)));

さて、なんとなく見えてきました。No.2を評価していくとこうなります。

a = a + (a = a + (a = a + (a = a + a)));
a = 1 + (a = a + (a = a + (a = a + a)));
a = 1 + (a = 1 + (a = a + (a = a + a)));
a = 1 + (a = 1 + (a = 1 + (a = a + a)));
a = 1 + (a = 1 + (a = 1 + (a = 1 + 1)));
a = 1 + (a = 1 + (a = 1 + (a = 2)));
a = 1 + (a = 1 + (a = 1 + 2)); // a = 2
a = 1 + (a = 1 + (a = 3)); // a = 2
a = 1 + (a = 1 + 3); // a = 3
a = 1 + (a = 4); // a = 3
a = 1 + 4; // a = 4
a = 5;

前から順に評価されていることがポイントです。実際、やや書き直したコードは意図したとおりに動いてくれます。

a = (a = (a = (a = a + a) + a) + a) + a
a = (a = (a = (a = 1 + 1) + a) + a) + a
a = (a = (a = (a = 2) + a) + a) + a
a = (a = (a = 2 + a) + a) + a // a = 2
a = (a = (a = 2 + 2) + a) + a // a = 2
a = (a = (a = 4) + a) + a // a = 2
a = (a = 4 + a) + a // a = 4
a = (a = 4 + 4) + a // a = 4
a = (a = 8) + a // a = 4
a = 8 + a // a = 8
a = 8 + 8 // a = 8
a = 16

考察

この実験から、 a += ba = b + a ではなく a = a + b と等価であり、連結させるとわけがわからない(ように見える)挙動をすることがわかりました。直感的にはどう考えても a += b += a += b は「aにbを足して代入。bにaを足して代入。aにbを足して代入。」という順序であるのが自然ですよね!?ちょっとこれは仕様策定がテキトーすぎませんかね。まあこんな変態じみたことする機会なんてそうそうないですしいいですけど。

おまけ

XORで交換する手法は圧縮するとおそらくこれが最も短いです。

// before
a^=b;b^=a;a^=b;
// after
b^=a^=b,a^=b;

a^=ba=b^a なら、あと2バイトも削れるのに…!!

おまけ その2

ふと書いていて思ったのですが、"代入演算子" は一般に=だけではなく+=なども含みます。では分割代入と組み合わせたらどうでしょうか?

let a = 1, b = 2;
[a,b] += [1,2];

Node v8.1.0で試したところ、「Invalid left-hand side in assignment」とエラーが起きました。訳すと「代入時の不正な左辺」ですかね。要するに「+=の左辺が構文的におかしくなってるで」ということです。まあ、a+=ba=a+b と等価なのを考えると [a,b]=[a,b]+[1,2]になっているわけですからそりゃ構文エラーですよね。

でもこれができたら行列計算なんかがすごく便利ですよね。新しい仕様として提案してみようかな、これ。