2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScriptで連番:range関数コードゴルフ

Last updated at Posted at 2021-02-26

はじめに

この間友人たちと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]

パフォーマンス計測

jsbench.meにベンチマークとコードあります。

今回の本題ではなかったですが、実用を見据えて一応計測しておきました。

計測

  • 乱数は最初に一気に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により、ベンチマークができなかった。ランタイムを破壊するな。。。

参考リンク集

2
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?