Iterableなオブジェクトを配列風に扱うためのライブラリを作った
JS標準オブジェクトのArray
風にIterable
なオブジェクトを操作するライブラリ(Iteration-JS
)を作ってnpmで公開しました。
このライブラリの持っている機能は先行で作ってる人が千人くらいはいると思います。1 2
型定義ファイルを同梱しているのでTypeScript環境でも利用可能です。
大元のソースはTypeScriptで書いてあるのでDeno環境からなら直接インポートすることもできます。
リポジトリ(GitHub,npm)
ソースコードはGitHubのリポジトリで管理しています。
ルート階層にソースが大量に置いてありますが、これはDeno環境などから直リンクで取得しやすい様にする工夫です。3
https://github.com/felis392/iteration-js/
https://www.npmjs.com/package/@felis392/iteration-js
Issueを上げてもらえれば対応するかもしれません。
機能の一例
リポジトリのREADMEを読んでもらえば全部書いてあるのでここでは一部だけ抜粋します。
中間操作でArray
は作られないので入力が100万件あってもメモリ消費は小さく抑えられます。
Iteration
クラスはItrableプロトコルを実装しているので必要があればArray.from()
で配列を得ることもできます。
import { Iteration, rangeClosed, iterate } from '@felis392/iteration-js';
const lcm = Iteration.on(rangeClosed(1, 1000000))
.filter(i => i % 17 === 0)
.filter(i => i % 19 === 0)
.filter(i => i % 23 === 0)
.findFirst(i => i % 29 === 0);
console.log(lcm);
// 215441
const total = Iteration.on(rangeClosed(1, 10000))
.filter(i => i % 17 === 0)
.filter(i => i % 19 === 0)
.reduce((r, e) => r + e, 0);
console.log(total);
// 150195
let i = 0;
for (const n of iterate(0, n => n < 10000000000000, n => n + 1)) {
i += n;
if (i >= 9000) {
console.log(`n = ${n} i = ${i}`);
break;
}
}
// n = 134 i = 9045
実装の解説
この記事のここから先は興味がある人向けです。ライブラリの利用がしたいだけならば読まなくても問題ありません。
ソースの全体はGitHubの方を見てもらえば分かるのでここでは一部のソースを例に解説を加えます。
ジェネレータ関数を活用する
下記の関数では1行目でいきなりreturn文を書いていますが、この関数はIterable
の実体としてGenerator
を返しています。
GeneratorFunction
が欲しい訳ではないのでジェネレータ関数式はその場で実行しています。4 5
始端操作と中間操作はこの様にジェネレータ関数を活用して実装しています。
export function iterate<T>(
seed: T,
hasNext: (v: T) => boolean,
next: (v: T) => T
): Iterable<T> {
return function* (seed, hasNext, next) {
for (let v = seed; hasNext(v); v = next(v))
yield v;
}(seed, hasNext, next);
}
委譲を多用する
ライブラリ内で定義済みの関数は別の関数の実装に利用できる場面であれば積極的に利用し、処理を委譲しています。6
特にIteration
クラスは薄いラッパーに徹するためファクトリメソッド以外はすべて単品の関数を呼び出す形になっています。
単品の関数はどれも単独で使えるものです。
利用したい場面が多そうなrange()
関数は以下の様に実装しています。
先程の説明で登場したiterate()
関数を呼び出す事で繰り返し処理の部分の実装を繰り返し行うのを回避しています。
// @deno-types="./iterate.d.ts"
import { iterate } from './iterate.js';
export function range(
start: number,
end: number
): Iterable<number> {
start = Math.floor(start);
end = Math.floor(end);
return start < end
? iterate(start, i => i < end, i => i + 1)
: iterate(start, i => i > end, i => i - 1);
}
単体テストを行う
TypeScriptをテストするツールはこれと言ったものが見つけられなかったため、ESモジュールに変換してからテストしています。7
こちらの記事で紹介されているtiny-esm-test-runnerを利用してテストを実施しています。8
import { range } from '../range.js';
import { assert } from 'tiny-esm-test-runner';
const { is } = assert;
export function test_ToPositive() {
const v = function*() {
for (const i of range(8, 13))
yield i;
}();
is(8, v.next().value);
is(9, v.next().value);
is(10, v.next().value);
is(11, v.next().value);
is(12, v.next().value);
is(undefined, v.next().value);
}
export function test_ToNegative() {
const v = function*() {
for (const i of range(4, -3))
yield i;
}();
is(4, v.next().value);
is(3, v.next().value);
is(2, v.next().value);
is(1, v.next().value);
is(0, v.next().value);
is(-1, v.next().value);
is(-2, v.next().value);
is(undefined, v.next().value);
}
以上
-
JS標準のArrayの他にJava標準ライブラリのStream APIなども参考にして実装している。お手本があるのだから似たものは山程あるはず。 ↩
-
npmリポジトリにパッケージを公開してみたいという動機が先にあって、どうせなら実用的なものを作ろうという事でこの題材になった。 ↩
-
ディレクトリ構成はLoDashやUnderScoreJSを参考にした。それらがどういう理由で今の構成になっているのかまでは調べていない。 ↩
-
exportしないジェネレータ関数を外で定義しておけばいいので別に関数式にこだわる必要はない。イディオムとしてマイブームだったので多用しているだけだったりする。 ↩
-
見て分かる様にJavaの
Stream.iterate()
を真似して作った関数。冒頭の例の様にfor...of
文に投入するなら普通にfor
文書いた方が見易いのは言うまでもない。 ↩ -
完全な最適化はしていないが、テストケースを書いてあるのでリファクタリングは気軽に行える。 ↩
-
JSにトランスパイルしてから行うならjestが使えそうだがESモジュールと相性が悪いのか設定の書き方が悪いのか上手く行かなかった。テストツールなんぞで悩みたくないので今回は気軽さ重視にした。 ↩
-
テストランナーがグローバルに注入してくるオブジェクトではなく明示的にアサーション用関数をimportするので非常に分かりやすい。 ↩