はじめに
この間友人たちとPythonでコードゴルフに興じる機会があった。
それでおもしろくなったので、今回JavaScriptでPythonのrange関数の実装をコードゴルフしてみることにした。
※ひとりコードゴルフ。
2021/02/27 修正
まともな動作検証をやっていなかったため見事にバグのご指摘をいただき、急遽簡単にテストしました。
ただ手を抜いているので、まだバグはあると思います。ご了承の上、発見の際はぜひご報告ください。
修正に伴い、記事の一部を書き直しました
修正点
- バグの発見された5つの関数(解説欄に修正と記してあります)
- 記法のおすすめ
- 負の値に関する暗黙のレギュレーションの明文化
以下は修正されていません
- パフォーマンスの計測値
レギュレーション
- 最短コードでrange(start,stop,step)を実装する。
- 出力はリスト・イテレータを問わない
- 負の値は考慮しない(start, stop, step, 出力する連番すべて0より大きいとする)
というわけで書いてみました。いろんなサイト様を参考にさせていただきましたが、もっといろんな方法で書けるやろ!と思ったのでやってみました。結果としては7+1通り。
コード内のa,b,c
は順にstart, stop, step
です。stopは含みません。
f(a,b,c)の形で頻出します。
ex: f(1,7,3) -> start 1, stop 7, step 3, -> [1, 4]
パフォーマンス計測
今回の本題ではなかったですが、実用を見据えて一応計測しておきました。
計測
- 乱数は最初に一気に100回分生成。それぞれの関数に100回ずつ計測。この100回を1試行とする。
- 7関数あるので7試行。試行間での乱数セットは共通。
- あとはjsbench.meにお任せ。
乱数の値域は以下
- start 0~49
- stop 51~100
- step 0~10
結論
以下作品集をつらつらしゃべる前に、簡潔にthe bestを知りたい人へ。僕から言いたいこと。
連番リスト
2021/02/27 追記
修正により評価が変わりました。連番リストは以下もおすすめしておきます。
[...Array(b-a)].flatMap((_,i)=>i%c?[]:i+a)
以下の記法よりも可読性が高く、長さも1字しか変わらないです。ただしパフォーマンスは低いので、小規模なリストのみの利用に向きます。
連番リストおすすめは
[...Array((b-a)/c+1|0)].map((_,k)=>a+k*c)
理由は、パフォーマンスがそこそこある、短くて書きやすい、アレンジがきく、ってこと。
Array(~)で個数を指定して、mapで中身をいじってる。ちょっと可読性が低いけど、まぁこれが無難な選択。
range関数
range関数でいうと、
function*f(a,b,c){do{yield a}while((a+=c)<b)}
もうまちがいなくこれ。なんならこの記事の中で一番自慢したいとこ。パフォーマンスも可読性も最強だからほんとにこれは言うことがない。強いて言えば取り回しが上の連番リストよりは多少劣ることぐらい。判定をちょっと変更すれば負のstepにも対応できる。
結構いろいろ見たけど、この簡潔な関数をなぜか誰も書いてない。みんな似たり寄ったりの連番リストばっかり。ぜひ使ってほしい。もちろん俺も使う。
やってることは、ほんとに単純なジェネレータ。それだけ。
優秀作品集
var f=(a,b,c)=>[...Array((b-a)/c+1|0)].map((_,k)=>a+k*c)
var f = (a, b, c) => [...Array(((b - a + c) / c) | 0)].map((_, k) => a + k * c);
2021/02/27修正
// 56字(出力部分だけなら41字)
// 安直な作り。出力部のみの利用が良さそう。
// ビット演算は少数部分のカットをしている。可読性がやや低いのが難点。
// この呼び方だとArrayの初期化に展開子...
が必要
// 必要最小限だけのリストを用意し、中身を調整する仕組み。
// パフォーマンスもかなり高い。しかし修正により価値を落とした。
// 31224.07 ops/s ± 3.34%
// 65.69 % slower
var f=(a,b,c)=>[...Array(b).keys()].filter(x=>!(x<a|(x-a)%c))
var f = (a, b, c) =>
[...Array(b).keys()].filter((x) => !((x < a) | (x - a) % c));
// 61字(出力部分46字)
// 大量に確保して一気に間引く方式。filter関数のビット演算は論理ORの代わり。
// 1813.2 ops/s ± 4.02%
// 98.01 % slower
var f=(a,b,c)=>[...Array(b-a)].flatMap((_,i)=>i%c?[]:i+a)
var f = (a, b, c) => [...Array(b - a)].flatMap((_, i) => (i % c ? [] : i + a));
2021/02/27 修正
// 57字(出力部分41字)
// 上2つの中間。ある程度搾っておいて、flatmapで間引きと調整を同時に行う。
// flatmapで空リストを返すと、その要素の消去と同じ挙動になる。
ロジックの見直しにより、可読性と短さを両立した便利な記法であることが判明。パフォーマンスは低い。
// 924.95 ops/s ± 3.88%
// 98.98 % slower
var f=(a,b,c)=>({[Symbol.iterator]:_=>({next:_=>({value:a,done:(a+=c)-c>b})})})
var f = (a, b, c) => ({
[Symbol.iterator]: (_) => ({
next: (_) => ({ value: a, done: (a += c) - c > b }),
}),
});
2021/02/27 修正
// 79字(出力部分のみでの利用不可)
// イテレータでも作ってみたかった。結果としては、イテレータ仕様に制約が多く、長くなった。
// 7922.87 ops/s ± 7.94%
// 91.29 % slower
function*f(a,b,c){do{yield a}while((a+=c)<b)}
function* f(a, b, c) {
do {
yield a;
} while ((a += c) < b);
}
// 45字(出力部なし)
// まさかの最小コード部門優勝はジェネレータ。可読性もめちゃくちゃ高く、芸術賞も同時受賞。
// さらにはパフォーマンス最速も叩き出し、最速部門賞まで受賞。まさかまさかの3冠達成。
// ただし実用面では、リスト方式の出力部のみの利用に取り回しの面で劣る。
ちなみにwhile判定を<b
から!==b
に変更すれば、「b-aがcで割り切れないと無限ループする」という制約のもと負のstepをとれるようになります。
// 90999.3 ops/s ± 5.87%
// Fastest
var f=(a,b,c)=>[..."".padEnd(b-a,10**c/10)].flatMap((v,i)=>-v?i+a:[])
var f = (a, b, c) =>
[..."".padEnd(b - a, 10 ** c / 10)].flatMap((v, i) => (-v ? i + a : []));
// 69字(54字)
// テキストメソッドをコンセプトに作ってみたもの。padEndの機能がおあつらえ向き。
// テキストの0,1を並べてstepを表現するアイデアはお気に入り。
// ただしパフォーマンスは最悪。
// 780.48 ops/s ± 9.71%
// 99.14 % slower
var f=(a,b,c)=>eval(`[${"".padEnd(b-a+c,0+",".repeat(c))}]`).flatMap((_,i)=>i+a)
var f = (a, b, c) =>
eval(`[${"".padEnd(b - a, 0 + ",".repeat(c))}]`).flatMap((_, i) => i + a);
2021/02/27 修正
// 80字(65字)
// テキストでarrayを構成してevalに突っ込む。リストのempty(!=undefined,!=null)のmapで参照されない特性を
// 生かしてstepを表現するという問題作。セキュリティ的にも脆弱?パフォーマンスは意外に悪くない
// 2160.94 ops/s ± 4.69%
// 97.63 % slower
番外
var f={a:2,b:10,c:3,next:_=>({value:f.a,done:(f.a+=f.c)-f.c>f.b}),[Symbol.iterator]:_=>f}
var f = {
a: 2,
b: 10,
c: 3,
next: (_) => ({ value: f.a, done: (f.a += f.c) - f.c > f.b }),
[Symbol.iterator]: (_) => f,
};
2021/02/27修正
// 89字(-)
// 自己参照イテレータによる実装。データ内蔵でイテレート機構まで完結する。
// 構造としては面白いが、名前と密結合しており、扱いづらさが目立つ。
// Out of memoryにより、ベンチマークができなかった。ランタイムを破壊するな。。。
参考リンク集
- https://qiita.com/ka215/items/354c6c405e015c49fcf8
-
JavaScriptでの「空」の値についてのおさらい - Qiita
JavaScriptでrange関数作ってみた - Qiita - 飛び飛びArrayのイテレート - Qiita
- jsでrange関数をつくる - Qiita
- 連番の数字の配列を作成(es2015 ver) - Qiita
- JavaScriptで[ 0, 1, 2, 3, 4 ]のような連番の配列を生成する方法 - Qiita
- https://gist.github.com/kemsakurai/2adc88292482a69690e128000c8afedb
- 演算子の優先順位 - JavaScript | MDN
- javascript - How to create an array containing 1...N - Stack Overflow
- Array.prototype.flatMap() - JavaScript | MDN
- 【JS】flatMapの使い方とreduceやfilterでの代用について | JavaScriptに関するお知らせ
- 【JS】nullでもundefinedでもない空配列(empty)の特徴と便利な使い方 | JavaScriptに関するお知らせ
- eval() - JavaScript | MDN
- String.prototype.padEnd() - JavaScript | MDN
- 【JS】JavaScriptでRange関数(Pythonのと全く同じ振る舞い) | JavaScriptに関するお知らせ
- JavaScript の range | Delft スタック
- arrays - Does JavaScript have a method like "range()" to generate a range within the supplied bounds? - Stack Overflow
- Javascriptでrange()みたいに指定長の配列を生成 - 動かざることバグの如し
- Generator.prototype.next() - JavaScript | MDN
- yield - JavaScript | MDN
- 反復処理プロトコル - JavaScript | MDN
- function* 宣言 - JavaScript | MDN
- yield* - JavaScript | MDN
- JavaScriptで小数を整数にするマニアックな方法とその実行速度 | q-Az
- JavaScriptで0からn-1までを要素にもつArrayを作成する方法 - blog.scheakur.com
- javascript - JavaScriptで特定の範囲の数値の配列を作りたい - スタック・オーバーフロー
- JavaScript - 連番付き変数をfor文のような繰り返し処理で実行させたい|teratail