javascript: リンターな max statements (とか complexity) との格闘と IIFE との愉快な関係

誤解されたら困るんでワタシから聞いたとは言わないで。

ESLint でもなんでもいいけれど「長過ぎる」「複雑過ぎる」なお行儀チェックは、整理がある程度まで進んできたら出来るだけ実施したいわけだね。とにかく完成させる・動くものを作る、を優先してると結構複雑なのを平気で書きがちだけれど、そういうことを続けてると「書いた本人さえ理解出来ないプログラム」になる。

ESLint の場合だと Limit Cyclomatic Complexityenforce a maximum number of statements allowed in function blocks が該当する。

そうなんだけどさ、enforce a maximum number of statements allowed in function blocks のデフォルト、厳し過ぎなんじゃないのかね、10 statements がデフォルトなのだわよ。一般に、は知らんけれど、「普通のエンジニアにとって」は、たぶん「エディタでスクロールが必要になるならバカデカ過ぎ過ぎ」というのはおそらく揺るがなくて、なので20~30ステートメント程度が「うわぁやだ」と感じる閾値なんじゃないのかなぁ、と思うんだけれどねぇ。実際多少でも「現実的」なプログラミングであるならば、それくらいの量の機能まとまりは「ごくごく普通」だし、ついでにいえば「誤った機能分割ほど厄介なものはない」のであってだな、たいていの場合は「分けりゃいいってもんじゃない」ルールに従って、綺麗な分割が見つかるまでは闇雲に分割しない、てのも「普通の感覚」なわけなんだね。

このやだなぁをちょっとだけマシにするために導入したのがこんな:

  1 function NodeVisFilterSettings() {
  2     //
  3     function _to_valarray(selector) {
  4         return $(selector).map(function () {
  5             return $(this).val().trim();
  6         }).get();
  7     }
  8     function _csv_split(selector, conv) {
  9         return $(selector).val().split(",").filter(function (e) {
 10             return e.trim();
 11         }).map(function (e) {
 12             if (!conv) {
 13                 return e.trim().toUpperCase();
 14             }
 15             return conv(e.trim());
 16         });
 17     }
 18     //
 19     let _this = this;
 20     this.settings = {"a": {}, "c": {}, "v": {}};
 21     //
 22     this._from_ui_a = function () {
 23         let target = _this.settings["a"];
 24         //
 25         target["aired_thr"] = implhelper.build_yyyymm_range_from_ui(
 26             $("#aired-year-min-thr").val(),
 27             $("#aired-month-min-thr").val(),
 28             $("#aired-year-max-thr").val(),
 29             $("#aired-month-max-thr").val(),
 30             "-");
 31         target["prem_thr"] = implhelper.build_yyyymm_range_from_ui(
 32             $("#prem-year-min-thr").val(),
 33             $("#prem-season-min-thr").val(),
 34             $("#prem-year-max-thr").val(),
 35             $("#prem-season-max-thr").val(),
 36             "");
 37         //
 38         let animetypesincl =
 39             $("#animetypes-incl-or-excl-incl").is(":checked");
 40         target["animetypesincl"] = animetypesincl;
 41         let animetypes_sel =
 42             animetypesincl ? "#animetypes-incl" : "#animetypes-excl";
 43         target["animetypes"] = _csv_split(animetypes_sel);
 44         //
 45         let genresincl = $("#genres-incl-or-excl-incl").is(":checked");
 46         target["genresincl"] = genresincl;
 47         let genres_sel = genresincl ? "#genres-incl" : "#genres-excl";
 48         target["genres"] = _csv_split(genres_sel, function (e) {
 49             let k = Object.keys(_datmng.master["gnr"]).find(
 50                 (key) => (key.toLowerCase() === e.trim().toLowerCase()));
 51             return _datmng.master["gnr"][k];
 52         });
 53         //
 54         let producersincl =
 55             $("#producers-incl-or-excl-incl").is(":checked");
 56         target["producersincl"] = producersincl;
 57         let producers_sel =
 58             producersincl ? "#producers-incl" : "#producers-excl";
 59         target["producers"] = _csv_split(producers_sel, function (e) {
 60             return _datmng._urlbuilder.malid2id(e.trim());
 61         });
 62         //
 63         target["popularity-thr"] =
 64             _to_valarray("#popularity-min-thr, #popularity-max-thr");
 65         target["ranked-thr"] =
 66             _to_valarray("#ranked-min-thr, #ranked-max-thr");
 67         target["episodes-thr"] =
 68             _to_valarray("#episodes-min-thr, #episodes-max-thr");
 69         target["duration-thr"] =
 70             _to_valarray("#duration-min-thr, #duration-max-thr");
 71         target["favorites-thr"] =
 72             _to_valarray("#anime-favorites-min-thr, #anime-favorites-max-thr");
 73         //
 74         target["rating"] = [
 75             "#rating-g",
 76             "#rating-pg",
 77             "#rating-pg13",
 78             "#rating-r",
 79             "#rating-rp",
 80             "#rating-rx",
 81             "#rating-none",
 82         ].filter(function (sel) {
 83             return $(sel).is(":checked");
 84         }).map(function (sel) {
 85             return $(sel).val();
 86         });
 87     };
 88     //
 89     this._from_ui_c = function () {
 90         let target = _this.settings["c"];
 91         //
 92         target["chara-ms"] = $("#chara-ms").val();
 93         //
 94         target["favorites-thr"] =
 95             _to_valarray(
 96                 "#chara-favorites-min-thr, #chara-favorites-max-thr");
 97     };
 98     //
 99     this._from_ui_v = function () {
100         let target = _this.settings["v"];
101         //
102         target["bloodtypes"] = _csv_split("#cv-bloodtypes");
103         //
104         let nativesincl = $("#natives-incl-or-excl-incl").is(":checked");
105         target["nativesincl"] = nativesincl;
106         let natives_sel = nativesincl ? "#natives-incl" : "#natives-excl";
107         target["natives"] = _csv_split(natives_sel);
108         //
109         target["birth-thr"] =
110             _to_valarray("#cv-birth-min-thr, #cv-birth-max-thr");
111         //
112         target["favorites-thr"] =
113             _to_valarray(
114                 "#actor-favorites-min-thr, #actor-favorites-max-thr");
115         //
116         target["actor-acting-total-thr"] =
117             _to_valarray(
118                 "#actor-acting-total-min-thr, #actor-acting-total-max-thr");
119         //
120         target["actor-acting-main-thr"] =
121             _to_valarray(
122                 "#actor-acting-main-min-thr, #actor-acting-main-max-thr");
123     };
124     //
125     this.from_ui = function () {
126         _this._from_ui_a();
127         _this._from_ui_c();
128         _this._from_ui_v();
129     };
130     //
131 }

入力コンポーネントから値をかき集めて構造に詰め込むだけの class で、当たり前だが入力フィールド数に「正比例」するわけね、ステートメント数が。無論「いろいろ命名規則を導入してどうにかループ処理で記述する」ことでもコンパクトには出来るけれど、そういうことではなく「html に書いてる id が剥き身で見えてた方がわかりやすい」とかいろいろあるでしょう、だから「ループした方が数十億倍ステキだ!」てわけでもない。愚直には愚直の良さがある。

すなわち「max-statements」は、こういった「単純移送処理」で露呈しやすいというわけだね。そしてこうしたものは「長いから万死に値する」という類のものでもないわけだ。どちらかといえば「長くなってもこればっかは致し方ないもの」だ。長くていやらしいのは「単純な繰り返しでない複雑なロジック」であって、むしろそうしたものだけが max-statements で引っかかるようであって欲しいわけだね。


で、結構長い時間このチェックを使ってたので随分前から気付いていたんだけれど、この max-statements のメトリクスってさ、「ほんとに function 単位」なのよね:

1 function foo() {
2   var bar = 1; // one statement (outer)
3   var baz = 2; // two statements (outer)
4   var qux = function () {
5       let x = 1; // one statement (inner)
6       let y = 2; // two statement (inner)
7       let z = 3; // three statement (inner)
8   }; // three statements (outer)
9 }

なので forEach やらなんやらの callback として匿名関数(というのかな?)で書いてると、その匿名関数内ローカルな statements 計測をするし、その forEach の外からみれば、どんだけその匿名関数がバカデカかろうが「one statement」なわけね。

というわけで…、IIFE てしまえば「誤魔化せる」わけなのよ:

  1 function NodeVisFilterSettings() {
  2     //
  3     function _to_valarray(selector) {
  4         return $(selector).map(function () {
  5             return $(this).val().trim();
  6         }).get();
  7     }
  8     function _csv_split(selector, conv) {
  9         return $(selector).val().split(",").filter(function (e) {
 10             return e.trim();
 11         }).map(function (e) {
 12             if (!conv) {
 13                 return e.trim().toUpperCase();
 14             }
 15             return conv(e.trim());
 16         });
 17     }
 18     //
 19     let _this = this;
 20     this.settings = {"a": {}, "c": {}, "v": {}};
 21     //
 22     this._from_ui_a = function () {
 23         let target = _this.settings["a"];
 24         //
 25         (function () {
 26             target["aired_thr"] = implhelper.build_yyyymm_range_from_ui(
 27                 $("#aired-year-min-thr").val(),
 28                 $("#aired-month-min-thr").val(),
 29                 $("#aired-year-max-thr").val(),
 30                 $("#aired-month-max-thr").val(),
 31                 "-");
 32             target["prem_thr"] = implhelper.build_yyyymm_range_from_ui(
 33                 $("#prem-year-min-thr").val(),
 34                 $("#prem-season-min-thr").val(),
 35                 $("#prem-year-max-thr").val(),
 36                 $("#prem-season-max-thr").val(),
 37                 "");
 38         }());
 39         //
 40         (function () {
 41             let animetypesincl =
 42                 $("#animetypes-incl-or-excl-incl").is(":checked");
 43             target["animetypesincl"] = animetypesincl;
 44             let animetypes_sel =
 45                 animetypesincl ? "#animetypes-incl" : "#animetypes-excl";
 46             target["animetypes"] = _csv_split(animetypes_sel);
 47         }());
 48         //
 49         (function () {
 50             let genresincl = $("#genres-incl-or-excl-incl").is(":checked");
 51             target["genresincl"] = genresincl;
 52             let genres_sel = genresincl ? "#genres-incl" : "#genres-excl";
 53             target["genres"] = _csv_split(genres_sel, function (e) {
 54                 let k = Object.keys(_datmng.master["gnr"]).find(
 55                     (key) => (key.toLowerCase() === e.trim().toLowerCase()));
 56                 return _datmng.master["gnr"][k];
 57             });
 58         }());
 59         //
 60         (function () {
 61             let producersincl =
 62                 $("#producers-incl-or-excl-incl").is(":checked");
 63             target["producersincl"] = producersincl;
 64             let producers_sel =
 65                 producersincl ? "#producers-incl" : "#producers-excl";
 66             target["producers"] = _csv_split(producers_sel, function (e) {
 67                 return _datmng._urlbuilder.malid2id(e.trim());
 68             });
 69         }());
 70         //
 71         (function () {
 72             target["popularity-thr"] =
 73                 _to_valarray("#popularity-min-thr, #popularity-max-thr");
 74             target["ranked-thr"] =
 75                 _to_valarray("#ranked-min-thr, #ranked-max-thr");
 76             target["episodes-thr"] =
 77                 _to_valarray("#episodes-min-thr, #episodes-max-thr");
 78             target["duration-thr"] =
 79                 _to_valarray("#duration-min-thr, #duration-max-thr");
 80             target["favorites-thr"] =
 81                 _to_valarray("#anime-favorites-min-thr, #anime-favorites-max-thr");
 82         }());
 83         //
 84         (function () {
 85             target["rating"] = [
 86                 "#rating-g",
 87                 "#rating-pg",
 88                 "#rating-pg13",
 89                 "#rating-r",
 90                 "#rating-rp",
 91                 "#rating-rx",
 92                 "#rating-none",
 93             ].filter(function (sel) {
 94                 return $(sel).is(":checked");
 95             }).map(function (sel) {
 96                 return $(sel).val();
 97             });
 98         }());
 99     };
100     //
101     this._from_ui_c = function () {
102         let target = _this.settings["c"];
103         //
104         target["chara-ms"] = $("#chara-ms").val();
105         //
106         target["favorites-thr"] =
107             _to_valarray(
108                 "#chara-favorites-min-thr, #chara-favorites-max-thr");
109     };
110     //
111     this._from_ui_v = function () {
112         let target = _this.settings["v"];
113         //
114         target["bloodtypes"] = _csv_split("#cv-bloodtypes");
115         //
116         let nativesincl = $("#natives-incl-or-excl-incl").is(":checked");
117         target["nativesincl"] = nativesincl;
118         let natives_sel = nativesincl ? "#natives-incl" : "#natives-excl";
119         target["natives"] = _csv_split(natives_sel);
120         //
121         target["birth-thr"] =
122             _to_valarray("#cv-birth-min-thr, #cv-birth-max-thr");
123         //
124         target["favorites-thr"] =
125             _to_valarray(
126                 "#actor-favorites-min-thr, #actor-favorites-max-thr");
127         //
128         target["actor-acting-total-thr"] =
129             _to_valarray(
130                 "#actor-acting-total-min-thr, #actor-acting-total-max-thr");
131         //
132         target["actor-acting-main-thr"] =
133             _to_valarray(
134                 "#actor-acting-main-min-thr, #actor-acting-main-max-thr");
135     };
136     //
137     this.from_ui = function () {
138         _this._from_ui_a();
139         _this._from_ui_c();
140         _this._from_ui_v();
141     };
142     //
143 }

ESLint 設定をどうしてるかによるけどもともと 25 とかでエラーだったのが、IIFE することでエラーが消える、と。


さて、ここからが「誤解してもらっては困る」の本題話。

「リンターを黙らせるために IIFE するのは悪なのかどうか」の話。無論「少しは性能悪くなったりするかもね」てのもあるだろうから、「なんも考えず適用しようとするなら悪だ」ろう。けどそれもわかった上でそうするとして、それでも十分に悪か?

これでワタシが思い知らされた通り、function だけがスコープを作るので、つまり「IIFE をスコープを作るためだけに利用する」ことが出来るだろう。そして、「なんで名前付きの function を作らないことがあるのか」が「自明でない」ことはないだろう、「汎用にはなりえないスペシャルでローカルな機能まとまり」には名前はそう易々とは付けられないし、そうしたものは「名前を付けて独立させるとかえって見通しが悪くなって保守しづらくな」りがちなわけだ。(実際にワタシの例で、IIFE した全てにいちいち名前を付けて独立させることを想像してみるがいい。)

つまり、まずは「スコープ限定化による局所化になるので嬉しい」という動機付けになりつつ、「名前を付けるまでもないが意味的なまとまりにはなる」ということならば、「リンターを黙らせる」ということではない「動機となる」とこじつけることが出来ますわな。これって実際ワタシが C/C++/java で良くやってたこれ:

 1 /* */ {
 2     int x = 1;
 3     int y = 2;
 4     hoge1(x, y);
 5 }
 6 /* */ {
 7     int x = 3;
 8     int y = 4;
 9     hoge2(x, y);
10 }

とノリは一緒なんだよね。(xUnit で特によく使う。)でも C/C++/java なリンターで「ブロック単位」というステートメント数計測に基づくお行儀チェックなんて発想はないけどね。


というわけでワタシは「こういった単純移送系ではリンターを黙らせたい」のために IIFE を多用しようかなと思う。そうすることで、「本当にコンパクトにしたい問題処理の検出」とそのリファインに集中出来るであろう、というわけだ。

ということなんだけどね。たださぁ…、このまさに「function 単位の計測」でのがまさしく仇になって、「本当は滅茶苦茶複雑で自分でも読むのがイヤになるようなもの」ほど案外このチェックに引っかからなかったりすんのがねぇ。なぜって、そういう「非常に複雑で読解困難」なものほど細かい function をたくさん作って呼び出してるから(Array の forEach だらけの処理がまさしく)。なので正直もうひとつ「トップレベルからの計測」も欲しいなぁ、なんてことは思ったりする。