javascript - の iterator がちょっと非力だったので


js-xiterable というモジュールを書いたというお話です。

mapもねえ、filterもねえ、そもそもそれほど使われてねえ?

例えばこういうジェネレーターがあったとして

function* count(n) {
    for (let i = 0; i < n; i++) yield i;
};

Arrayにしたり…

[...count(8)]; // [0,1,2,3,4,5,6,7]

ループを回したり…

for (const i of count(8)){
  console.log(i);
}

というところまではいいとして、

count(8).map(v => v*v).filter(v => v % 2 === 1)

とかと書けないのはがっかりしませんか?他は

Swiftも…

(0..<8).map{$0 * $0}.filter{$0 % 2 == 1}

Rubyも…

(0..7).map{|v| v*v}.filter{|v| v % 2 == 1}

かなり毛色が違うとはいえ Python も…

[v * v for v in range(8) if v % 2 == 1]

できるのが当たり前なのですから。

できる!そう js-xiterable ならね

というわけで出来るようにしてみました。こんな感じで使います。

import {Xiterable} from './xiterable.js';
const xcount = n => new Xiterable(() => count(n));
const tens = xcount(10);
const odds = tens.filter(v=>v%2).map(v=>v*v);
const zips = tens.zip(odds);
[...tens];  // [ 0,      1,      2,       3,       4, 5, 6, 7, 8, 9]
[...odds];  // [ 1,      9,     25,      49,      81]
[...zips];  // [[0, 1], [1, 9], [2, 25], [3, 49], [4, 81]]

徹頭徹尾 lazy

xiterable なオブジェクトは常に lazy です。要素は本当に必要になるまで一切生成されません。

Swift や Ruby では

(0..<8).map{$0 * $0} // この時点で [0, 1, 4, 9, 16, 25, 36, 49]
 が生成される

ので

(0..<Int(1e8)).map{$0*$0}

とかやると要素数 1e8 == 1億のArrayを生成しようとしてしまいます。これを防ぐには

(0..<Int(1e8)).lazy.map{$0*$0}

と明示的にlazyにしてあげるとよいのですが、xiterable場合はじめからlazyなので

xcount(1e8).map(v => v*v)

は瞬殺ですし、

[...xcount(1e8).map(v => v*v).take(8)]

も即座に[0,1,4,9,16,25,36,49]と評価されます。

さらにランダムアクセス可能な無限イテレーターという概念までサポートしていて、例えば


import { xrange } from './xiterable.js'
xrange().nth(42);  // 41

なんてこともできます。xrange()というのはPythonとかのrange()と同様なのですが、引数がない場合には[0,1,2...]を無限に生成するジェネレーターとして機能します。しかし.nth()で任意の番目の要素が取得できるという。

BigInt もサポート

もう一つの特長として、番号指定にNumberだけではなくBigIntも指定可能ということが挙げられます。例えば

[...xrange(0,4)];  // [0,1,2,3]
[...xrange(0n,4n)];// [0n,1n,2n,3n]

なぜそうなっているかといえば、Number.MAX_SAFE_INTEGER == 9,007,199,254,740,991より大きな番号も扱う必要が出てきたからです。実はjs-xiterableを書く前にjs-combinatoricsも全面的に書き直したのですが、例えば'abcdefghijklmnopqrstuvwxyz'の順列(permutation)の総数は26!== 403,291,461,126,605,635,584,000,000にもなり、Numberどころかuint64_tでも足りません。

しかしBigIntはすでにSafari 13を除くモダンブラウザーでサポートされており、Safariも14からはサポートすることが決定しています。使い始めるにはいい時期だと判断した次第です。

import { Permutation } from 'js-combinatorics';
let it = new Xiterable(
  new Permutation('abcdefghijklmnopqrstuvwxyz')
);
it.map(v => v.join('')).nth(403291461126605635583999999n); 
// 'zyxwvutsrqponmlkjihgfedcba'

念のため、js-xiterablejs-combinatoricsBigIntがなくても動きます。Number.MAX_SAFE_INTEGER以上の番号は扱えませんが。

あとは

Githubをご覧くださいませ。enjoy!

Dan the Lazy JavaScripter