JavaScriptでPythonのzip関数を実装してみました

Python3にはzip()という便利な組み込み関数があります。
以下のように、複数のイテラブル[^1]を同時にイテレーションできる関数です。

```Python
chars = ["a", "b", "c"]
nums = [1, 2, 3, 4, 5]

for elems in zip(chars, nums):
    print(elems)

# 出力
# ('a', 1)
# ('b', 2)
# ('c', 3)
```

引数にイテラブルを複数指定すると、各イテラブルの要素をまとめたタプルを返すイテレータを作ってくれます。
イテラブル間で要素数が異なる場合は、もっとも短いイテラブルの要素が尽きた時点で止まります。

この関数をJavaScriptで再現してみます。

# 環境と背景知識

* ES6（ES2015）の機能を使います。モダンブラウザやNode.jsの新しめのバージョンであれば通用する内容です。たぶん。
* JavaScriptの[ジェネレータ関数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/function*)や[反復処理プロトコル](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Iteration_protocols)、[スプレッド構文や残余引数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax)などについての知識を使います。



# 先行事例

「JavaScript Python zip」で検索すると似たような試みは何件かヒットします。
Qiitaでも[先駆者](https://qiita.com/QUANON/items/c1cf22fda7c7813cc962)がいらっしゃいます。

個人的にいつも参考にしている[民主主義に乾杯](https://python.ms/javascript--zip/)さんから実装例を引用します。

```JavaScript
function* zip(...args) {
    
    const length = args[0].length;
    
    // 引数チェック
    for (let arr of args) {
        if (arr.length !== length){
            throw "Lengths of arrays are not eqaul.";
        }
    }
    
    // 
    for (let index = 0; index < length; index++) {
        let elms = [];
        for (arr of args) {
            elms.push(arr[index]);
        }
        yield elms;
    }
}

```

空の配列elmsを作る→イテラブルのそれぞれi番目の要素を取得して順次elmsにpush→elmsをyield、を繰り返す流れです。
配列間の要素数が同じである必要はあるものの、Pythonのzip関数と同じようなことができそうです。
イテレータを返すところも再現度が高くて良いですね。

[yucatio](http://yucatio.hatenablog.com/entry/2020/02/06/085930)さんの実装もご紹介します。

```JavaScript
const zip = (...arrays) => {
  const length = Math.min(...(arrays.map(arr => arr.length)))
  return new Array(length).fill().map((_, i) => arrays.map(arr => arr[i]))
}
```

こちらはジェネレータ関数ではなく普通の関数（アロー関数）としての実装で、返すのはイテレータではなく配列になります。
この実装が優れている点の一つは、配列間の要素数が違っていても、最も短い配列に合わせて処理してくれるところです（2行目）。
3行目はmap()が入り乱れていたり、謎のfill()が挟まっていたりと少々読みづらいですが、元サイトで詳しく解説されているのでご覧になってみてください。

<details><summary>先行事例についてさらに考察しましたが、ちょっと長くなったので折りたたみ。
要約すると、「先行事例のコードでは一部のイテラブルが期待通りに動かないよ」という内容です。</summary><div>

## 改良してみた

上記二つのコードを組み合わせてみます。


```JavaScript
function* zip(...args) { 
    const length = Math.min(...args.map(arg => arg.length));
    for (let index = 0; index < length; index++) {
        let elms = [];
        for (const arr of args) {
            elms.push(arr[index]);
        }
        yield elms;
    }
}
```

引数のイテラブルの要素数が異なっても対応でき、さらにPythonのzip()関数と同じくイテレータを返すようになっています。
これでなかなかの再現度になったのではないでしょうか？

使ってみましょう。

```JavaScript
const chars = ["a", "b", "c"];
const nums = [1, 2, 3, 4, 5];

for (const elems of zip(chars, nums)) {
    console.log(elems);
}

// 出力
// Array [ "a", 1 ]
// Array [ "b", 2 ]
// Array [ "c", 3 ]
```

Pythonと同じ結果が得られました！

また、Pythonのzip()はリスト[^2]以外でも、文字列などイテラブルであれば何でも引数にとれます。
JavaScript版zip()でも文字列を引数にして試してみます。

```Javascript
const str = "qwe";
const nums = [1, 2, 3, 4, 5];

for (const elems of zip(str, nums)) {
    console.log(elems);
}

// 出力
// Array [ "q", 1 ]
// Array [ "w", 2 ]
// Array [ "e", 3 ]
```

いけますね。
その他、NodeListやargumentsオブジェクトなどのイテラブルにも使うことができます。
このzip関数ならどんなイテラブルが来ても怖くありません。

…**本当に？**

## 添字アクセスができないと使えない

たしかに、JavaScriptの代表的なイテラブルである配列や文字列は問題なくイテレートできます。
それでは、先ほどのzip()でSetオブジェクトをイテレートしてみます。

```Javascript
const str = "qwe";
const numSet = new Set([1, 2, 3, 4, 5]);

for (const elems of zip(str, numSet)){
    console.log(elems);
}

// 出力
// 
```

…ダメですね。期待通りにイテレートしません。
Setオブジェクトは間違いなくイテラブルのはずなんですが。

原因はzip()の定義の6行目、`elms.push(arr[index]);`の部分で行われている添字アクセスです。
Setオブジェクトでは、`numSet[1]`のような添字を使った形で要素にアクセスすることができない[^3]ので、意図した通りに処理されないのです。

Mapオブジェクトやジェネレータオブジェクトも同様に、イテラブルかつ添字アクセスができないオブジェクトです。

</div></details>

# 本編です

折りたたみを読んでくださった方、ありがとうございました。ここから本編です。
折りたたみを飛ばした方、大丈夫です。本編はここからです。

今回作るzip()は、Pythonのzip()のように以下の要件を満たします。

* 引数のイテラブルをまとめてイテレートする機能を提供する
* 実行したらイテレータを返す
* イテレートはもっとも短いイテラブルの要素が尽きたら終了する
* イテラブルならなんでも、いくつでも対応可能である

## 方針

Pythonの[公式ドキュメントのzip()の項目](https://docs.python.org/ja/3/library/functions.html#zip)を見てみると、ありがたいことにzip()と等価なコードが紹介されています。
引用します。

```Python
def zip(*iterables):
    # zip('ABCD', 'xy') --> Ax By
    sentinel = object()
    iterators = [iter(it) for it in iterables]
    while iterators:
        result = []
        for it in iterators:
            elem = next(it, sentinel)
            if elem is sentinel:
                return
            result.append(elem)
        yield tuple(result)
```

このコードのzip()は組み込みのほうのzip()と等価なので、当然先ほど挙げた要件を満たします。
これをJavaScriptで書き直せばよさそうです。

## 解読します

上のコードの処理を解読します。

全体としては中にyieldがある関数宣言の形ですので、ジェネレータ関数です。

```Python
sentinel = object()
```

どういうわけか`object()`で空のオブジェクトsentinelを生成しています。
この段階では何のためかわかりません。とりあえず保留します。

```Python
iterators = [iter(it) for it in iterables]
```

引数のイテラブルからiter()でイテレータを取得して、iteratorsという名前のリストに格納しています。
リスト内包表記でiterablesの全ての要素について一括で処理していますね。
[イテラブル1, イテラブル2, イテラブル3,…]から[イテレータ1, イテレータ2, イテレータ3,…]を作っている感じです。

続いて`while iterators:`でループが始まっています。
もしiteratorsが空ならここはスルーされてなにもyieldされないので、空のジェネレータオブジェクトになります。

```Python
#ループ内
result = []
for it in iterators:
    elem = next(it, sentinel)
    if elem is sentinel:
        return
    result.append(elem)
yield tuple(result)

```


ループの中では、ループ毎にresultという空の配列が作られています。
そのあとfor文でiteratorsを走査しています。
具体的には、「イテレータからnext()で値を取り出す→resultに追加」を全てのイテレータについて行っています。
resultへの追加が完了したら、タプルに変換してyieldしています。

ここで、始めに作られた謎のsentinelがnext()の第二引数に使われていますね。
[next()のドキュメント](https://docs.python.org/ja/3/library/functions.html#next)を確認します。

> next(iterator[, default])
> iterator の \_\_next__() メソッドを呼び出すことにより、次の要素を取得します。イテレータが尽きている場合、 default が与えられていればそれが返され、そうでなければ StopIteration が送出されます。

`next(it, sentinel)`の直後にif文で`elem is sentinel`が評価されていることを考え合わせると、イテレータが尽きているかどうかをnext()の結果がsentinelかどうかで判断しているようです。
つまり、「イテレータが尽きる→`elem is sentinel`がTrueになる→returnしてジェネレータを終了」ということ。
「もっとも短いイテラブルの要素が尽きたら終了」という仕様がこういう形で実現できるんですね。

## いざ、JavaScriptで実装

大事な点が一つ。
機能的には、JavaScriptでは`イテレータ.next()`が、Pythonの`next(イテレータ)`に相当します。
しかし、両者の戻り値は異なります。
Pythonの`next(イテレータ)`は値を直接返します（例えば、ジェネレータならyieldされた値をそのまま返します）。イテレータが尽きていた場合、StopIteration を送出するか、第二引数の値を返します。
一方、JavaScriptの`イテレータ.next()`が返すのは、valueとdoneというプロパティを持つオブジェクトです。イテレータが実際に返した値はこのオブジェクトのvalueプロパティを参照して取得します。また、イテレータが尽きていた場合、doneプロパティがtrueになります（そうでない場合はfalseです）。

Pythonのコードではイテレータが尽きたかチェックするのにnext()の第二引数を利用していましたが、JavaScriptでは戻り値のdoneプロパティを参照すればよいので空オブジェクトsentinelを用意する必要はありませんね。

ではコードを書きます。
ジェネレータ関数なので`function*`宣言の形にします。

```JavaScript
function* zip(...iterables) {

}
```

次にiteratorsを作りますが、JavaScriptにはリスト内包表記はないのでmap()で代用。
また、イテラブルからイテレータを取得する`iter(it)`に相当するのは、JavaScriptでは`it[Symbol.iterator]()`[^4] [^5]です。

```JavaScript
const iterators = iterables.map(it => it[Symbol.iterator]());
```

あとはwhileの部分です。
JavaScriptとPythonのnext()の違いに注意。
もう一つ注意が必要なのが、`while`の条件式です。Pythonでは空のリストはFalsyですが、JavaScriptでは空の配列はTruthyです。したがって、そのまま`while (iterators)`と書くわけにはいきません。
また、Pythonではresultをタプルにしてからyieldしていますが、JavaScriptにタプルはないので配列のままyieldします[^6]。

```JavaScript
while (iterators.length) {
    const result = [];
    for (const it of iterators) {
        const elemObj = it.next();
        if (elemObj.done) {
            return;
        }
        result.push(elemObj.value);
    }
    yield result;
}
```

以上を組み合わせれば完成です。

## 完成

```JavaScript
function* zip(...iterables) {
    const iterators = iterables.map(it => it[Symbol.iterator]());
    while (iterators.length) {
        const result = [];
        for (const it of iterators) {
            const elemObj = it.next();
            if (elemObj.done) {
                return;
            }
            result.push(elemObj.value);
        }
        yield result;
    }
}
```

## 使ってみます

配列・文字列・Setオブジェクト・Mapオブジェクトを、今回作ったzip()でイテレートしてみます。

```JavaScript
const chars = ["a", "b", "c"];
const str = "qwe";
const numSet = new Set([1, 2, 3, 4, 5]);
const numMap = new Map([[1, 1], [2, 4], [3, 9], [4, 16]]);

for (const elems of zip(chars, str, numSet, numMap)) {
    console.log(elems);
}

// 出力
// Array(4) [ "a", "q", 1, Array [ 1, 1 ] ]
// Array(4) [ "b", "w", 2, Array [ 2, 4 ] ]
// Array(4) [ "c", "e", 3, Array [ 3, 9 ] ]
```

期待通りの動作です。



[^1]: この記事では、PythonのイテラブルオブジェクトとJavaScriptの反復可能オブジェクトをまとめて「イテラブル」と呼ぶことにします。
[^2]: この記事では、JavaScriptの配列をPythonのリストに相当するものとして扱います。
[^3]: JavaScriptのブラケット表記はプロパティアクセサーなので、`numSet[1]`のように書くと「numSetというオブジェクトの1というプロパティ」にアクセスすることはできます（通常このようなプロパティは未定義なのでundefinedになります）。しかし、普通はこれをSetオブジェクトの要素へのアクセスとは見做さないでしょう。
[^4]: もしこのメソッドに馴染みのない方がおられましたら、MDNの[反復処理プロトコル](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Iteration_protocols)の記事をご覧ください。
[^5]: `it[Symbol.iterator]()`です。`it.Symbol.iterator()`ではありませんので注意してください。
[^6]: 変更不可能な配列として返すだけなら`yield Object.freeze(result)`とすれば実現できますが、あまり意味はないと思います。