Panda Noir

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

分割代入の評価順に関する研究

以前、Fisher-Yatesアルゴリズムについての記事を書きました。先日、記事中のコードをより短くできないか?とふと思い検証してみたところ、「分割代入の評価順と代入演算子を悪用すればいいんじゃないか」と思いつきました。今回はそのテクニックを紹介しようと思います。

分割代入と入れ替え

分割代入のよくある使い方に変数の交換があります。

[a, b] = [b, a]; // 変数同士の入れ替え

また、配列内での要素の交換もできます。

[arr[i], arr[j]] = [arr[j], arr[i]]; // 配列内での入れ替えも可能

Fisher-Yatesでは配列内での交換が重要なポイントとなっています。

今回のコードゴルフの題材コード

まず普通のFisher-Yatesの実装がこちら

const shuffle = arr => {
    for (let i = arr.length; i >= 0 ; i--) {
        const j = 0 | Math.random()*(i+1);
        [arr[i], arr[j]] = [arr[j], arr[i]];
    }
};

このコードではjという変数が出てきます。しかし変態プログラマの我々はjの代入部分の返された値を利用したくなります。

ここで問題となるのは「分割代入は左辺と右辺のどちらから評価されるのか」です。

つまり、上のforループ内は

[arr[i], arr[j = 0 | Math.random()*(i+1)]] = [arr[j], arr[i]];
[arr[i], arr[j]] = [arr[j = 0 | Math.random()*(i+1)], arr[i]];

のどちらと等価なのか?ということです。

分割代入の評価順

どうせなので左辺、右辺それぞれの中の評価順も見てみます。

const alrt = n => {
    alert(n);
    return n;
};
const arr = [1,2,3,4,5];
[arr[alrt(1)], arr[alrt(2)]] = [arr[alrt(3)], arr[alrt(4)]];

これを実行すると「3→4→1→2」の順に表示されます。ということは「右辺の要素を順に評価し、次に左辺の要素を順に評価する」ということです。

つまりさきほどのコードは

const shuffle = arr => {
    for (let i = arr.length, j; i >= 0 ; i--) {
        [arr[i], arr[j]] = [arr[j = 0 | Math.random()*(i+1)], arr[i]];
    }
};

にすると意図した通りに動くということです(jの宣言はforに組み込みました)。

終わりに

いやーこんな初見で読めないクソコードを他人に書かれたらブチギレ案件ですね。使うときは用量、用法を守ってください。