javascript: 続々・リンターな max statements (とか complexity) との格闘

IIFE は関係あるようなないような。

アプリケーション本題の進化も進めつつ eslint の max statements はデフォルトの 10 で NG にならない状態を一応維持し続けている。当然ながら IIFE だけがこれへの措置というわけはなく、色んな小技を駆使して max statements と格闘している。2つ前からの続きの話として、3つばかり気付いたことがあったので、一応。


一つ目の話は 最初の話から直接導出出来る話だったりはする。

つまり、「計測は function 単位なので」という事実に従うだけで max statements 制限を迂回することが出来ることが非常に多いわけなのだが、とりわけ「map, filter などを多用する」のは、わかりやすくもなってええかもしらんな、と思ったという話。以下は全体は非常に長いのに max statements で NG にならない、実際のワタシのヤツの例:

IIFE も適用しているがそれよりも map, filter が効果大
 1 MalPageParser.prototype.parse_person_detail = function (
 2     basinfpart, result, defail_fields) {
 3     let _this = this;
 4     (function () {
 5         basinfpart = basinfpart.replace(/<\/?div>/g, "\n")
 6             .replace(/<br\s*\/?>/g, "\n")
 7             .replace(/\n\s+/g, "\n")
 8             .replace(/<\/?span>/g, "");
 9     }());
10     let kvpairs = [];
11     (function () {
12         let ki = [];
13         (function () {
14             let rgx = new RegExp(
15                 "\n([^(][^\n]+:|Also\\s[Kk]nown\\sas)\\s", "g");
16             let m;
17             while (m = rgx.exec(basinfpart)) {
18                 ki.push([[m.index, m[0].length], m[1].replace(":", "")]);
19             }
20         }());
21         ki.forEach(function (idxk, i) {
22             let st = idxk[0][0] + idxk[0][1];
23             let ed = (i < ki.length - 1) ? ki[i + 1][0][0] : basinfpart.length;
24             kvpairs.push([
25                 idxk[1], // key
26                 basinfpart.slice(st, ed).split("\n").map(e => e.trim()),
27             ]);
28         });
29     }());
30     result["dtl"] = {};
31     kvpairs.map(function ([k, v]) {
32         return [
33             _this._key_norm_and_compact(k, "person"),
34             v
35         ];
36     }).filter(function ([k, _]) {
37         if (k === "more") {
38             return false;
39         }
40         if (["family_name", "given_name"].includes(k)) {
41             return true;
42         }
43         // if not specified, then delete all...
44         if (defail_fields.length === 0 ||
45             !defail_fields.includes(k)) {
46             return false;
47         }
48         return true;
49     }).map(function ([k, v]) {
50         if (k === "aka") {
51             return [k, v.join(" ").trim()];
52         }
53         return [k, v[0]];
54     }).map(function ([k, v]) {
55         if (k === "brth") {
56             return [k, MalDataTypes.parse_date(v)];
57         }
58         if ((new RegExp('<a href=".*?">(.*?)</a>')).exec(v)) {
59             return [k, _this._split_hreflist(v, "auto")];
60         }
61         let m = new RegExp("^#?([0-9,]+)").exec(v);
62         if (m) {
63             return [k, implhelper.relaxed_parse_int(m[1])];
64         }
65         return [k, v];
66     }).map(function ([k, v]) {
67         if (["altn", "nickn", "aka", "realn", "brthn"].includes(k)) {
68             return [
69                 k,
70                 v.replace(/([()\uff08\uff09]|\sor\s)/g, ", ").split(
71                     new RegExp(",\\s")).map(e => e.trim()).filter(e => e)
72             ];
73         }
74         return [k, v];
75     }).forEach(function ([k, v]) {
76         result["dtl"][k] = v;
77     });
78 };

これだけ長いと確かにイヤにはなるのだけれど、ただ区切った filter, map などが各々比較的明快に小さく書けているので、見かけの大きさほどは読みにくくはない。こうやって「小さなタスクに分割してチェイン」していく設計にしたこと自体が「大きな保守性の改善」そのものなんだけれど、それを「名前付きの関数をたくさん作ること」ではない方法で整理を進められるのはありがたい、というわけだ。


2つ目の話は、こんなヤツの話:

1 const MalUrl = new function () {
2     this.omit_proxy = function (url) {
3         // ...
4     };
5     // ... 以下たくさんの、プライベートメソッドを含むメソッド
6 };

これ、「function 単位なので」に従って、「いっぱいメソッド作っちゃえ」とするとやはり max statements 制限に引っかかってしまう。うーん、新しい ES の class にしちゃったほうがいいのかなぁ、と思うのはこんなときなのかもしらんね。

もともと22個のメソッドを持ってたんだけど、最初はこんなふうに整理を始めた:

 1 const MalUrl = new function () {
 2     const _impl = new function () {
 3         //  ここにプライベートなメソッドたち
 4     };
 5 
 6     this.omit_proxy = function (url) {
 7         return _impl.omit_proxy(url);
 8     };
 9     // ... 以下たくさんのメソッド
10 };

まぁこれで済むうちはこれだけでもいいんだけれど、これをやっても 17、18 程度にしかならず。最終的には普通に分離:

1 const _MalUrlImpl = new function () {
2     //  ここにプライベートなメソッドたち
3 };
4 const MalUrl = new function () {
5     this.omit_proxy = function (url) {
6         return _MalUrlImpl.omit_proxy(url);
7     };
8     // ... 以下たくさんのメソッド
9 };

まぁこうしたことは別に javascript に限ったものではないよね。(ワタシは C++ 脳が発達してるので C++ でいうところの「匿名 namespace にヘルパーをまとめる」のを思い出す。)


最後の話は、「ずっと思ってたけど見つけられずにいて、さっきやっとわかった」ヤツ。『「うぅ、all が欲しい」と思ってたが「every なのかよ」』。

何かつーと。「if の羅列」パターンね、たとえばワタシのではこんな:

 1     this._filter_a_by_types = function (detail) {
 2         let target = _this.settings["a"];
 3         if (target["animetypes"].length &&
 4             target["animetypesincl"] !== target["animetypes"].includes(
 5                 detail["type"].toUpperCase())) {
 6             return false;
 7         }
 8         if (target["genres"].length &&
 9             (detail["gnr"].findIndex(function (e) {
10                 return target["genres"].includes(e);
11             }) >= 0) !== target["genresincl"]) {
12             return false;
13         }
14         if (detail["ratg"] &&
15             !target["rating"].includes(detail["ratg"])) {
16             return false;
17         }
18         return true;
19     };

まさにフィールド数に「正比例」してステートメント数が増えていくパターンだ。判定そのものはそれぞれ微妙に少しずつ違うことをするので簡単に一撃関数に出来るようなもんではない。また、「キーが違うだけ」という程度の差異しかないとして、キー名の規則を活用して整理する、というのも「良いかどうかは場合による」、つまり「愚直だからこそわかりやすい」ということもある。つまり、「このコードの見かけをほとんど変えず、なおかつ効率も損なうことなく function 呼び出しの結合にしたいわけなのだ。

「Python の all 相当のもの」がずっと見つけられてなくて、それだけが理由で保留にしてたが、もし「all 相当のこと」が出来るなら、上の if 一個ずつが function になっていて、その function たちを保持する Array に対して「all 相当のこと」をすれば、同じことが出来るはずだ、というわけだ。そう、「なんだよ、every かよ」:

 1     this._filter_a_by_types = function (detail) {
 2         let target = _this.settings["a"];
 3         return [
 4             function () {
 5                 return (target["animetypes"].length &&
 6                         target["animetypesincl"] !== target["animetypes"].includes(
 7                             detail["type"].toUpperCase()));
 8             },
 9             function () {
10                 return (target["genres"].length &&
11                         (detail["gnr"].findIndex(function (e) {
12                             return target["genres"].includes(e);
13                         }) >= 0) !== target["genresincl"]);
14             },
15             function () {
16                 return (detail["ratg"] &&
17                         !target["rating"].includes(detail["ratg"]));
18             }
19         ].every(function (fn) {
20             return !fn();
21         });
22     };

IIFE ではないよ、念のため。そして function を定義してそれを実際に実行するのは every のコールバック内なので、真にならないものを見つけ次第 every は処理をやめる。つまり効率の面では元の if の羅列とほぼ同じだということに着目すべし。(必要なければ呼び出さない。つまり判定に合致次第即 return している元のコードとノリが完全に同じ。)

max statements に引っかかりにくくなるだけでなくて、保守もしやすくなるよねこれ。フィールドが増えた場合は function を増やしていくだけだから。(まぁ厳密には性能面で function 定義ぶんほんの少しだけ劣るんだろうけど。)