【Google Apps Script】FileIteratorとFolderIteratorをiterableにして使いやすくする

# 結論
最初に結論を言います。

これを

```js
const getGoogleFiles = () => {
  const files = DriveApp.getFiles();

  const result = [];
  while (files.hasNext()) {
    const file = files.next();
    const fileName = file.getName().toLowerCase();
    if (fileName.includes('google')) result.push(file.getUrl());
  }

  Logger.log(result);
};

```

wrapper関数を1つ噛ませてこう書く方が好きという話です。

```js
const getGoogleFiles = () => {
  const result = gasUtils.toIterable(
    DriveApp.getFiles(),
    {
      mapFn: (file) => file.getUrl(),
      filterFn: (file) => file.getName().toLowerCase().includes('google'),
    }
  );

  Logger.log([...result]);
```
ちなみにwrapper関数`toIterable`は以下です。

```js
const gasUtils = {
  /**
   * DriveAppのFileIteratorとFolderIteratorをiterableにする関数
   * @template {DriveApp.FileIterator | DriveApp.FolderIterator} GASIterator
   * @template {DriveApp.File | DriveApp.Folder} GASFileOrFolder
   * @param {GASIterator} iter - Google Apps Scriptのイテレータ
   * @param {{
   *   mapFn: (val: GASFileOrFolder, index: number) => any
   *   filterFn: (val: GASFileOrFolder, index: number) => boolean
   * }}
   * @return {Generator}
   */
  toIterable: (iter, {
    mapFn = (val) => val,
    filterFn = () => true,
  }) => {
    return {
      *[Symbol.iterator]() {
        let i = 0;
        while (iter.hasNext()) {
          const [nv, ni] = [iter.next(), i++];
          if (filterFn(nv, ni)) yield mapFn(nv, ni);
        }
      }
    };
  },
};
```
ご興味があればどぞ。

# はじめに
Google Apps Scriptは便利です。
ちょっとしたツールや自動化にはもってこいです。

Google系のサービスとは簡単に連携できます。
そんなわけで、Google Drive上のファイルやフォルダを操作したくなるわけです。
マイドライブの中でファイル名に`google`の文字が入るもののURLの一覧が取得したい…!
それもSpreadsheetに一発で貼り付けられるように配列で取得したい…!!

調べてみれば[`DriveApp`](https://developers.google.com/apps-script/reference/drive/drive-app)なるものが簡単に見つかります。
早速試してみましょう。

```js
/** !!これは動きません!! */
const getGoogleFiles = () => {
  // ファイルを全て取得
  const files = DriveApp.getFiles();
  
  // 名前に'google'を含むファイルのURLを取得
  const result = files.flatMap(file => {
    const fileName = file.getName().toLowerCase();
    return fileName.includes('google')? [file.getUrl()] : [];
  });
  
  // 出力を確認
  Logger.log(result);
};
```

これでエディタから`getGoogleFiles`を実行すればめでたく目標のものがとれるはず。
とっても簡単だぁ。
と思いきや、帰ってくるのはエラー。

`TypeError: files.flatMap is not a function`

な、なんだってー？！
それもそのはず、`DriveApp.getFiles`の返り値の型は`DriveApp.FileIterator`。
配列じゃないのです。[イテレータ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_Generators)なのです。


というわけで、目的の結果を得るには次のように書く必要があります。

```js
const getGoogleFiles = () => {
  // ファイルを全て取得
  const files = DriveApp.getFiles();
  
  // 名前に'google'を含むファイルのURLを取得
  const result = [];
  while (files.hasNext()) {
    const file = files.next();
    const fileName = file.getName().toLowerCase();
    if (fileName.includes('google')) result.push(file.getUrl());
  }
  
  // 出力を確認
  Logger.log(result);
};
```

うーん。なんともコレジャナイ感。

# 何が不満なのか

個人的な嗜好…もとい志向として以下が気に入りません

1. 最初に空の配列を定義して`while`ループで`push`するのがつらい。破壊的メソッドは滅ぶべし
2. 終端判定のメソッドが`hasNext`なところ。MDNのイテレータの説明では`done`なのに
3. iterableじゃない。反復可能ならスプレッド構文なんかで簡単に配列化できるのに

といわけで、iterable(反復可能)にするwrapper関数を挟めば使い勝手が良くなりそう。
どちらにせよwrapper関数はあった方が将来的な保守性が上がるので、やらない手はない。

## 余談 反復可能にする。どうやって？ <br> (ご存じの方は飛ばしてください)

JavaScriptのオブジェクトには2種類ある。
反復可能なオブジェクトと、反復可能ではないオブジェクトだ。

その違いは、**反復可能なオブジェクト**とは**`@@iterator`メソッド**が実装されているか否かです。

```js
// 反復可能じゃない
const notIterable = {
  hoge: 'hoge',
  fuga: 'fuga',
  piyo: 'piyo',
};

// 反復可能
const iterable = {
  [Symbol.iterator]: function* () {
    yield 'hoge';
    yield 'huga';
    yield 'piyo';
  }
};

// 省略記法 iterableと意味は同じ
const shortIterable = {
  *[Symbol.iterator]() {
    yield 'hoge';
    yield 'huga';
    yield 'piyo';
  }
};
```

この`*`がついている厳めしい関数はジェネレータと呼ばれるものです。
肩肘張るようなものでもなく、`yield`で値を返して実行を一旦停止する関数といった感じ。

重要なのはこの**`Symbol.iterator`という名前のメソッドがジェネレータで実装されていること**。
これが反復可能か、そうでないかの分かれ道です。
とにかく、この名前のジェネレータのメソッドを持っていればそのオブジェクトは反復可能です。

では反復可能だと何が嬉しいの？といったところですが、`for-of`やスプレッド構文で各値を取り出すことができます。

```js
// for-ofで各値を取り出せる
for (const item of iterable) {
  Logger.log(item);
  // expected output:
  // hoge
  // huga
  // piyo
}
  
// スプレッド構文で各値を取り出せる
Logger.log([...iterable]);
// expected output:
// [hoge, huga, piyo]

// for-ofで取り出せない
for (const item of notIterable) {
  Logger.log(item);
  // TypeError: notIterable is not iterable
  // 地味にこのエラーメッセージ面白い
}
```

察しの良い方はお気づきでしょうが、`Array`オブジェクトなんかには元々この`@@iterator`メソッドが実装されています。
とっても便利だぁ！

ともあれこの`@@iterator`メソッドがApps Scriptのイテレータ、厳密にいうと`DriveApp.FileIterator`や`DriveApp.FolderIterator`といったオブジェクトに実装されていないことが問題なわけです。

ならどうすればいいか？
**Apps Scriptのイテレータの各値を`yield`するジェネレータを`@@iterator`メソッドとして持ったオブジェクトを返す関数**があれば必要十分ですね。
そう考えると案外簡単かもしれない。


# Simple is best

複雑なのは良くないので、できるだけシンプルに反復可能性だけ付与します。

```js
// Apps Scriptのイテレータ類をiterableにする
const toIterable = (iter) => {
  return {
    *[Symbol.iterator]() {
      while(iter.hasNext()) yield iter.next();
    }
  };
};

const getGoogleFiles = () => {
  const files = toIterable(DriveApp.getFiles());
  
  const result = [...files].flatMap(file => {
    const fileName = file.getName().toLowerCase();
    return fileName.includes('google')? [file.getUrl()] : [];
  });
  
  Logger.log(result);
};

```

うむ。悪くない。
大分最初の姿に近づくことができました。
wrapper関数のコード量もそれほど多くなく、必要十分にシンプルだと思います。

ただ、ちょっと寄り道が多すぎるかもしれない。
特に、一旦配列にしてから`flatMap`するところ。

# ものごとはできるかぎりシンプルにすべきだ。しかし、シンプルすぎてもいけない。

先程の実装はスタート地点としてはこれ以上なく優秀でした。
しかし、もう少しwrapper関数に機能を追加すれば格段に便利かもしれません。

例えば、次のような`mapFn`と`filterFn`を追加してみるのはどうでしょう。

```js
// Apps Scriptのイテレータ類をiterableにする
const toIterable = (iter, { 
  mapFn = (val, index) => val, 
  filterFn = (val, index) => true,
}) => {
  return {
    *[Symbol.iterator]() {
      let i = 0;
      while (iter.hasNext()) {
        const [nv, ni] = [iter.next(), i++];
        if (filterFn(nv, ni)) yield mapFn(nv, ni);
      }
    }
  };
};

const getGoogleFiles = () => {
  const result = toIterable(
    DriveApp.getFiles(),
    {
      mapFn: (file) => file.getUrl(),
      filterFn: (file) => file.getName().toLowerCase().includes('google'),
    }
  );

  // これまでと同じ結果
  Logger.log([...result]);
};

```

`filterFn`で条件を満たしたものだけ`yield`され、`mapFn`の処理を通したものを取得します。
それほどwrapper関数のコード量も増えず、かつ本関数内の変数が`result`1つになります。
ループの回数も減るので、気持ちが穏やかになります。
ちなみにこのコード内では使用していませんが、`mapFn`と`filterFn`の二番目の引数でインデックスも使えるようにしています。こんなんなんぼあってもいいですからね。

**個人的にはこれが一番使いやすいと思います。**
`getGoogleFiles`の中の処理の見通しもこれが一番良く感じます。

# JSDocで補完が効くようしたり

Apps Scriptでは[JSDoc](https://www.typescriptlang.org/ja/docs/handbook/jsdoc-supported-types.html)を利用してエディタの補完を効かせるようにすることができます。
よって以下のようにすれば`mapFn`や`filterFn`の中でもきちんと補完が効くようになります。

```js
const gasUtils = {
  /**
   * DriveAppのFileIteratorとFolderIteratorをiterableにする関数
   * @template {DriveApp.FileIterator | DriveApp.FolderIterator} GASIterator
   * @template {DriveApp.File | DriveApp.Folder} GASFileOrFolder
   * @param {GASIterator} iter - Google Apps Scriptのイテレータ
   * @param {{
   *   mapFn: (val: GASFileOrFolder, index: number) => any
   *   filterFn: (val: GASFileOrFolder, index: number) => boolean
   * }}
   * @return {Generator}
   */
  toIterable: (iter, {
    mapFn = (val) => val,
    filterFn = () => true,
  }) => {
    return {
      *[Symbol.iterator]() {
        let i = 0;
        while (iter.hasNext()) {
          const [nv, ni] = [iter.next(), i++];
          if (filterFn(nv, ni)) yield mapFn(nv, ni);
        }
      }
    };
  },
};
```
もちろん関数の動作は変わりません。
が、しれっと`gasUtils`というオブジェクトで包みました。
なので、`getGoogleFiles`の`toIterable`の参照を`gasUtils.toIterable`に変えてください。
ちなみにこれをすると

- 名前空間が切れる
- エディタの実行関数候補から`toIterable`が消える

という効用があります。
**エディタから実行する予定がない関数は積極的にオブジェクトで包みましょう。**
みなが幸せになります。


# おわりに

大体伝えたかったことは以上です。
強いて言うなら速度比較とかもしたかったですが、とりあえずここまでで一旦投稿しておきます。
よろしくお願いします。