以前、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に組み込みました)。
終わりに
いやーこんな初見で読めないクソコードを他人に書かれたらブチギレ案件ですね。使うときは用量、用法を守ってください。