概要
Airbnb の JavaScript Style Guide を適用する eslint のプラグイン eslint-config-airbnb を入れてコーディングをすると、 for ... of を使うなと怒られる。仕方ないので、map とか forEach とか every とか使っているうちに、for..of を使わないでやっていけるようになったという話。
1. 状況整理
1-1. 怒られる理由
for..of を使うと eslint-config-airbnb が怒る。理由は no-restricted-syntax 。詳しいことは、Qiita の記事「ESLint対応物語 ~no-restricted-syntax~」 と、その記事へのコメントが詳しくて、抜粋すると以下。
for-of 構文を Babel (babel-preset-es2015) で変換すると regenerator-runtime が必要になるため、変換後のコードが巨大になります。利点欠点を比較すると、そこまでして使う利点はないよね、というお話。同時に、ループ文自体を避けてより目的にあった Array メソッドを使うのはベストプラクティスです
1-2. 回避できないの?
eslint の設定で簡単に回避できる。Qiitaの記事「ESLintのno-restricted-syntaxでfor-ofだけを許可」を参照。
私はこの方法は選ばず、Airbnb の軍門に下ることにした。つまり、for..of を使わないという選択をした。eslint の設定ファイルを毎回いじるのがめんどくさいというのが一番大きな理由かも。関数型プログラミングに拒否感がないのも理由に挙げられる。
1-3. for..of の代替
for..of を回避するのはいいとして、代替案はあるの?ということだけど、Airbnb のスタイルガイド では、以下を推奨している。
Use map() / every() / filter() / find() / findIndex() / reduce() / some() / ... to iterate over arrays, and Object.keys() / Object.values() / Object.entries() to produce arrays so you can iterate over objects.
まあ、よくある話で、反復処理は関数型なパラダイムで行いなさい ということらしい。
2. 代替をきちんと考える
2-1. 代替のポイント
特に何も考えなくても、以下のように書き換えれば大体の場合 for..of は使わなくて済む。じゃあ何が問題なのだろう?
// 書き換え前
for (const x of arr) {
// do something with x
}
// 書き換え後
arr.forEach((x) => {
// do something with x
});
二つ問題がある。一つ目は break の扱いだ。for 文は一つずつ取り出して何かをする際に使われるが、break 文を使うと途中でとりだすのをやめることができる。これによって、反復を打ち切ることによって計算量を削減したり、反復要素数が無限の場合(例: ジェネレータ)を扱えるようになる。一方、forEach, map, reduce では、どれも途中で打ち切ることはできない。
二つ目は反復対象の多様性だ。for..of はイテラブルプロトコルを実装したオブジェクトを反復対象とできるが、map, forEach, reduce などは Array のメソッドである。
2-2. breakと向き合う
まず考えたいのがそもそも打ち切らないという割り切りだ。要素数が数百程度なら、最近のCPUパワーでは問題にならないし、コードベースで複雑になるくらいなら、break自体をしないという選択はありだと思う。
さて、一番簡単な対策は for(;;) を使うことだ。これならeslintに怒られない。後述の対策が有効でないような場合は、常にこの選択肢にフォールバックする。とはいえ、積極的にfor(;;)を使いたいという人はいないだろう。
もし、途中で打ち切ることができるメソッドが用途があえばこれを使う。具体的には find(), some() を使う。意外とこれらでこと足りる場合は多い。反復の目的が、条件を満たす要素の存在確認だったりその要素を取り出す場合だ。
const scores = [{ name: 'alice', score: 40 }, { name: 'bob', score: 60 }, { name: 'chales', score: 80 }];
scores.find(obj => obj.score > 50); // -> { name: 'bob', score: 60 }
RamdaのReduced を使うという代替案もある。JavaScriptで本格的に関数型プログラミングをしようとすると、ramda を導入することが多いと思う。もし ramda が使える環境なら、Ramda の提供する reduce およびその reducer は、途中で打ち切ることができることを知っておくべきだ。map も reduce に書き換えができるので、Ramda を使えば map も reduce も途中で打ち切れる。
// NOTE: この例では、Array.prototype.find() を使った方がよいが・・・
const R = require('ramda');
const scores = [{ name: 'alice', score: 40 }, { name: 'bob', score: 60 }, { name: 'chales', score: 80 }];
const pickup = R.reduce((acc, obj) => (obj.score > 50 ? R.reduced(obj) : {}), {});
pickup(scores); // -> { name: 'bob', score: 60 }
2-3. Array以外の反復
前述のように for..of はイテラブルプロトコルを実装した任意のオブジェクトで使える。具体的には、配列、文字列、Map、Setがビルトインで提供されている。
配列・文字列 の反復は特に問題ない。
Map や Set の反復操作の場合は、Map.prototype.forEach
もしくはSet.prototype.forEach
が使えるし、配列に変換したいなら Array.prototype.from
に渡してあげたり、spread syntaxを使えば配列に変換できるので ramda と連携させることもできる。
3. やっぱ for..of いらないわ
for..of やめていいことがあるよ、ということを例を挙げてみよう。
for..of で囲まれた部分は、ある意味処理をベタ書きしているのと同じなので、少しでも長くなると読むのが負担になる(インデントが深くなると脳内レジスタが溢れてしまう)。一方、forEach, map, reduce はそうならない。もちろん、コールバック関数や mapper, reducer を匿名関数にするならば一緒なのだが、そうではなく、関数を別の場所で定義できるのが非常に大きなメリットになる。
const users = ['alice', 'bob', 'chales'];
const [listOK, listNG] = [[], []];
for (const user of users) {
try {
const address = getAddress(user);
const msg = createMessage(user, address);
sendmail(msg);
listOK.push(user);
} catch (err) {
emitWarnLog(`fails to send mail to ${user}, errmsg:${err}`);
listNG.push(user);
}
}
console.log(`OK:${listOK.length}, NG:${listNG.length}`);
上記の例は、for文のなかに try..catch が入ってきてるし、処理が結構長めなので読みづらい。for文のスコープの中で、listOK, listNG を操作しているというのもテストをしずらくする一因となっている。これを forEach で書き換えるとこうなる。
function doSend (user, OK, NG) {
try {
const address = getAddress(user);
const msg = createMessage(user, address);
sendmail(msg);
OK();
} catch (err) {
emitWarnLog(`fails to send mail to ${user}, errmsg:${err}`);
NG();
}
}
const users = ['alice', 'bob', 'chales'];
const [listOK, listNG] = [[], []];
users.forEach(user => doSend(user, x => listOK.push(x), x => listNG.push(x)));
console.log(`OK:${listOK.length}, NG:${listNG.length}`);
関数を外だしできたので、本文のインデントが 0 になった。 for..of のインデントが2 なので forEachを使うとインデントの深さを 2 小さくできた。また、doSendという名前をつけることができたので反復操作の目的が名前から推測できるようになった。
また、forEachでは、関数を渡せばいいのでくくり出せるのでテストがしやすくなった。listOKやlistNGを弄るのは厄介だぞっと実装時に気づいて、コールバックを引数に取るように doSend を実装できたのもポイントが高い。ふつうにやるとdoSend に listNG や listOK を参照渡しするようになってしまって、関数の純粋性が破壊されるだけでなく、バグの温床になったり、メンテナンス性や拡張性に問題を残すことになってしまっただろう。
4. まとめ
まあ、for..of なくてもなんとかなるよ。