lint系ツールの未使用引数警告の扱いって面倒だよね

有用だが繊細だ、の典型。

lint系ツールの未使用引数警告の扱いって面倒だよね

たわごと

経験上、数ある「有用なチェック」の中でもとりわけ繊細なんじゃないかと、やるたんびに思う。

なんでもそうだがこうした道具は「バカが使うと全てを台無しに」してしまうわけであるが、未使用引数チェックはその被害が最も簡単に露見しやすいんじゃないのかなぁと思うんだよね。

そもそも「大前提」を忘れているかもしくは単に「知らない」若者、あるいは「本当に学習しないバカ」に対して、多少なりともの説明をしないまま未使用引数のチェックを実施させれば、たぶん「まともな感性」を持ってなければ、ほとんど十中八九「引数を削除」してしまうであろう。そう、「大前提」というのは、「引数というのはインターフェイスである」ということだ。だから「未使用引数のチェック」に引っかかったコードを「削除することによって」措置することは、かなりの高確率で「アプリケーションをぶち壊す」というわけだね。

で、こうした「大原則」が、「どんな言語でも全く同じ」なのかと言うと、これがまた結構違うのも、悩ましいといえば悩ましい。


いわゆる C 系の言語で、特に「C89 以降の C の系譜」にある C 系ファミリの言語の場合、つまり、「プロトタイプ宣言が出来て、なおかつ、静的な型付けを行う言語」の場合、多くの場合は「型だけを書く」ことが出来る。つまり、こういう書き方が出来る:

1 int HOGEAPI_create_hoge(const HOGEAPI_CONTEXT* ctx. int /*currently, unused.*/)
2 {
3     // ...
4 }

C も C++ も java も、「引数リスト」は、受け取る数・各々の引数の型、のいずれかが違うものは「異なるもの」になるため、一度皆が使えるものとして公開してしまった後は、気軽に変更してはいけないわけである。気軽に変更しようものなら、あなたの顧客たちが束になって押しかけてきてバケツで泥水をぶっかけられることは間違いない。なので、例にしたように「今ではもう使っていない引数」を、コメントを付けつつも、「型だけ」書くことが許されている。つまり「未使用引数」の措置は、この手の言語の場合はまさにこれ。「引数」を省略する(型だけ書く)。

これを「キャーすてきー」というのは違うが、ただ、「引数リストの変更」の影響が元々大きいこの手の言語では、比較的エンジニア間のコンセンサスを構築しやすい方かもしれないな、とは思う。

もちろんこの警告をトリガーに、「そもそもこのインターフェイスは正しいのか?」に思いをめぐらすことは不可欠である。この警告は、であるから、「インターフェイスの見直しのチャンス」を示していることもある、というわけである。実際最初に考えた設計が粗く、必要のないものを受け取ろうとしていた、ということも起こりうるわけである。そしてさらに、「当たり前だが」、この未使用警告が「本物のバグ」であることがある、ということだ。未使用引数警告を受け取った場合に実に「真っ先に」考えなければならないことは無論「仕様どおりのことをしているのか?」なのであるから、たとえばドキュメントされた引数の用法があって、それでもなお「未使用」ならば、やはりそれは何か間違っていることになる。(要するに未使用引数のチェックというのは、まさにそのために使う。)

つまり…、「十中八九引数を削除してしまうであろう」という、浅はかなエンジニアにとっての「多数派決断」は、現実には「滅多にその決断は起こらない」。(仮にこれが言い過ぎでも、「削除が対処になる」割合は、その種のエンジニアが考える半分どころか1/10にも満たないのが普通。)


動的な言語の場合は、無論「インターフェイスであるのだからして」という根本的なところで大きく違うというわけではないのだが、やはり言語によっても色々違った見え方をする。

Python の場合は、特に注意すべきは「引数」もインターフェイスの一部である、という点である。つまり:

1 def some_3dcalc(x=0, y=0, z=0):
2     return x**3 + y**2 + z
3 
4 some_3dcalc(1, 2, 3)  # 位置引数指定で呼び出せる、が
5 some_3dcalc(y=3) # 特定の引数を選んでも呼び出せる (キーワード引数)

つまりキーワード引数を期待して呼び出すクライアントコードを破壊しないためには、引数名も気軽に変えてはいけない。キーワード引数が使える言語では共通の話なので、たぶん Visual Basic でもそうじゃないかな? 使ったことがないので聞きかじりレベルだが、遠い記憶でキーワード引数が使えた記憶がある。

で、こうした言語の場合で未使用引数の警告をもらった場合何を考えるかと言えば、正直言ってしまえば「静的言語と、後半部分(つまり「措置」以外)は全く同じである。気軽に変えればクライアントからの電話は鳴り止まなくなる。そして当然のことながら、「型だけを書く」なんてことは出来ない。型なんか書けないんだから。(Cython でない限りは。)

Python に限った場合、「可読性は落ちる」というデメリットはあるものの、こういう手がないではない:

1 def some_3dcalc(*args, **kwargs):
2     x = ... # args と kwargs から頑張ってほじくる (args は配列、kwargs は辞書)
3     return x**3 + y**2 + z
4 
5 # クライアントコードは概ね壊れない
6 some_3dcalc(1, 2, 3)  # 位置引数指定で呼び出せる
7 some_3dcalc(y=3) # 特定の引数を選んでも呼び出せる (キーワード引数)

ただ「未使用引数警告を喰らったので」という理由でこれをするのは、「どちらかと言えば愚か」だろう。こうしたインターフェイスにしたい動機はほとんどの場合は「3、4個では引数リストが足りない」場合にでのみ主に正当化されるべきものであって、この警告だけを理由にそうすることは大変よろしくない。「ほじくる」の部分、自分でどういう実装になるか、やってみればわかると思うぞ。ついでにいえば引数リストだけからは何を受け取ってくれるのかわからなくなるので、ドキュメンテーション文字列をより真剣に書かなければならない、どうしてもそれが必要な場合には。(*args, **kwags 方式が正当化されるもう一つの典型的な例が、いわゆる「フォワーダ」「プロキシ」のようなインターフェイスのもの。プロキシであれば、それが適合する相手のインターフェイス変更に追従し続けるコストが高くなりがちなので、*args, **kwags 方式で緩く受け取るわけである。)

なので、「*args, **kwargs は許容出来ない」ならば「インターフェイスを変えてはならない」場合にはもう術がなく、「諦める」しかない。つまり、「ごめん、インターフェイス変えなきゃいけなくなっちゃった」と(正式な手続きを要することが多いわけだが)謝りまくるなり、あるいは「措置保留」。(個人プロジェクトなら無頓着でいいかと言えばこれも違って、「呼び出しているコード全てを洗い出して影響をはかる」ことが必要。つまり本質は変わらない。)


で、今「オレ的本題」の javascript。Python に似ているところもあるし、全然似てない話もある。と今回思った。

先に Python で、上とは違った例を書いておく:

いわゆる「コールバック」タイプの指向
1 def invoke_your_fun(your_fun):
2     your_fun(1, 2, 3)

これも「your_fun のインターフェイス」の話ということになり、Python では話はここまで。「ダックタイピング」という側面はあるにせよ、とにかく invoke_your_fun は「your_fun は3つの引数を受け取るのだ」というお約束に基いて呼び出すわけである

まさにこれに相当するケースが、javascript ではだいぶ違った見え方をする。つまり:

1 nodes.forEach(function(elem, i) {
2     //
3 });

上の Python 例での「invoke_your_fun」に相当するのがこの場合 forEach で、その function が「要素とインデクスを」…、「受け取れ」。そう、「受け取りたくなければ受け取らなくていい」:

1 nodes.forEach(function(elem) { // index はいらん、elem だけでええんや
2     //
3 });

これ、今試してみたが「余分な引数を渡した場合は単に無視される」という仕様みたいだね、なので「受け取らない決断」が出来る。すなわち、javascript で未使用引数警告を受けた場合の、一つの措置になる、というわけだ。

ただ、「forEach のような頻出で暗記が容易い」ものでない場合がちょっとイヤらしくて、例えば:

1 $(".posint-spinner").spinner({
2     min: 0,
3     spin: function(event, ui) {
4         $(this).change();
5     }
6 });

このケースで、もしも「ワタシが jquery-ui に不慣れならば」(事実今はまさにそう)、「フルで受け取るとすればこれらを受け取れる」ということがドキュメント的に残っていることが大変ありがたく、なので「event, ui」を「今」使っていないからという理由でコードから削除してしまうのは結構勇気がいる。とするならばこんな感じなんだろうか:

1 $(".posint-spinner").spinner({
2     min: 0,
3     spin: function(/*event, ui*/) {
4         $(this).change();
5     }
6 });

それにしても、これを「便利だぁ」と思うかどうかは別の話なのだが、「渡す方」にも同じような話が当てはまるのが javascript。例えばこうだよね:

 1 function ore_wa_benrida(arg1, arg2, arg3) {
 2     if (arg1) {
 3         // arg1指定されてれば云々
 4     }
 5     if (arg2) {
 6         // arg2指定されてれば云々
 7     }
 8     if (arg3) {
 9         // arg3指定されてれば云々
10     }
11 }
12 // ...
13 ore_wa_benrida(1, 2, 3); // 全部渡す
14 ore_wa_benrida(1, 2); // arg3 は渡さない
15 ore_wa_benrida(1); // arg2 も渡さない
16 ore_wa_benrida(); // arg1 すら渡さない

似たことを C++ や java では「オーバロード」で可能だし、Python のデフォルト引数、*args, **kwargs も似たようなものなわけだけれど、若干というかかなりノリが違うのが、「受け手(ore_wa_benrida)の引数リストそのものは何か宣言的なことをしない」ということなわけだ。つまり、

Python のデフォルト引数
1 def ore_wa_sonnnaniwa(a, b="x"):  # クライアントが「あぁ、b のデフォルトが x なのね」とわかる
2     pass

というようなことではなく、魔法はあくまでも「function の実装に仕込む」ということなわけね。(このノリは Python ではまさに *args, **kwargs に相当。)

話を戻すと、じゃぁ、javascript で未使用引数の警告を受けた場合に、ほかの言語と違うか、と言えば、「大事なコア機能、ユーティリティ、といったものがインターフェイスを安直に変えるのはやっぱし結局ダメでしょ」というところについては大差はないのだけれど、ほかの言語よりはもっとずっとリラックスして考えても良さそうだ、ということにはなる。ただ…、「可読性」まで踏まえた場合に「良いことだ」とまで言えることは、たぶんそうない。だって、こんなのやだろ?:

1 function significant_some_api(a) {
2     // 昔は a, b, c, d, e, f の6つの引数を受け取っていたが、今は幸せなことにたった一つだ!
3     // 未使用引数チェック、ありがとう!!!!!!!!
4 }
5 //
6 significant_some_api(1, 2, 3, 4, 5, 6); // しらねーよ…

つまり結局のところは「気軽にインターフェイスをころころ変えんじゃねーよ、バーカ」は、javascript ですら「ほとんど同じ」ということ。無論この例は「全く問題なく動作」するのかもしれないが、多分呼び出し側の「期待」とは違ったことをするに違いないのである。だって「そう指示させてたじゃん、かつて」。(わかりやすい例は、例えば「omit_english」という引数がかつてあったとするならばクライアントコードは当然「英語はいらないのだ」ということを期待してそうしているはずで、そして「未使用引数チェック、ありがとう!!!!!」側が我関せず、ということだ、たとえば。)

結局のところ、見かけ上はほかの言語と「かなり」異なるものの、結局考えることはほとんど変わらない。「インターフェイスを簡単に変更してはならない」という大原則は、全く同じ。


結局のところ何が言いたいのかというと、「未使用変数警告を使いこなすには、教育が不可欠」ということなのかな、と思う。結構シャレにならんくらい見てきたからね、「未使用変数警告を鵜呑みにしてインターフェイスを破壊する若者」。ある程度の経験を積んだエンジニアはこれはさすがにあんまりやらなくて、比較的「技術的な格差」が出にくいところではあるんだけどね、けど「若手とベテラン」の差はむしろハッキリ出る気がする。

こういうお行儀チェック系のツールを皆に使わせるといつも感じるのって、「それをやりだすとそれしか見えなくなる」てことなんだよね。「未使用変数の警告」を受けて「その関数やメソッドを変更する」という決断に「クライアントコードへの配慮」が抜けるのって、根本的にソフトウェアエンジニアとしては致命傷を抱えているんだけど、なぜか「没頭しだすとそうした基礎を忘れる」のがかなり多い。無論教育されてなければ、「考えもしない」のも大量発生するというわけね。

で、「チェックツール適用にあたってのルール作りが必要だ」と二つ前で書いたけれど、今回のような話をそうしたルール記述にコンパクトに書くのってなんか難しいよなぁ、と思ってな。「一言でわかりやすく注意する」って、出来ないよなぁ、と。「それ以前にちゃんと教育せぃや」つーことだろ、だって。


なんてことを、「ただの遊びプロジェクトに適用してる ESLint」から色々思い出して書いてみました、ってオハナシ。

老害のたわごと、をしまい。

2018-02-11追記

賢いな、ESLint。以下:

1             select: function(event, ui) {
2                 var terms = this.value.split(/,\s*/);
3                 terms.pop(); // remove the current input
4                 terms.push(ui.item.value); // add the selected item
5                 // add placeholder to get the comma-and-space at the end
6                 terms.push("");
7                 this.value = terms.join(", ");
8                 return false;
9             }

今の場合「使ってない」という意味だと「event を使ってない」ことになる。ので、仮にこれで警告されたとしたら、これはどう頑張っても措置しようがない。「だって ui は使ってる」んだから。というケースを ESLint は「ちゃんと知ってる」のだね、このケースを未使用とみなさない。ありがたい。

2018-02-13追記

一つ目。一つ上の追記の「賢さ」については、これは { “args”: “after-used” } がデフォルトだから、だね。

で、ワタシのヤツで、上で言った「コメントアウトや削除で済むもの」に該当しない、ワタシの場合だと「元々使うつもりで、将来もそれをインターフェイスとするつもりだが、諸事情により just now では未使用」というヤツらが残った。

ワタシのヤツで特に特筆すべきヤツは「エキスポートデータのデータマイグレーション」のための function で、「将来絶対に必要になる」というつもりで書いてて、というかほんとにちょっと前まで実効コードがいたんだけれど、「公式リリース前にやってもしょうがない」性質のものを今頑張っても保守が大変なだけなので、「中身を削除」したのね、一時的に。なので、当然以下は no-unused-vars 警告を喰らう:

1     function _data_migration(orig, fmtver) {
2         // for the future.
3         return orig;
4     }

インターフェイスはこのままにしたいわけだよ。これを ESLint を正義として変えるなんて到底許容出来ない。とするならば…:

1     function _data_migration(orig, fmtver) {
2         // for the future.
3         if (!fmtver) {
4             throw new Error("Specify fmtver!");
5         }
6         return orig;
7     }

ひとまずこうか。まぁ正式公開後に実効コードを書いた後もそのまま使えるね、これなら。

で、こういう「正義」も「措置」もわかりやすいやつについてはいいんだけれど、もっと微妙なヤツもあって。つまり、「必要だと思って受け取っていたし、将来的にもおそらく必要になる」と思っているのに just now 使ってないものについては、「受け続けたいし、渡し続けたい」のだよね。ワタシのヤツだとこれ:

1 function _update_elements_style(force) {
2     // ...
3 }

何か中で条件をみて更新するかどうかを決めるという振る舞いを「普通は」しつつ、「状況によっては強制的に」ということが、最初本当に必要だった。今は「それをするほどの理由が見当たらないほどに force 有無の差がなくなってしまった」というだけの理由で force を見てない。けれどこの中でやってることの性質上「きっといずれまた必要になる」と考えている、ということ。

こういうのが弱るんだよね。昔からあるのだと「FALLTHROUGH」なんかがそうだけど、「これは意図である」とか指示したいんだけれど、「ESLint も JSLint も JSHint も使う」という決断を例えばしたとすると、「全部リンターごとの指示を書く」ことは避けたい。つまり、「たとえそう出来たとしてもインラインコメントでリンターを制御するのは避けたい」。けれども「これは意図である」というコメントは入れておきたい。

しかも「リンターを黙らせつつ「意図である」というコメントを入れる」という行為をしたいとして、例えば C++ の例で:

1 (void)func();  // 「戻り値を使っていない/見ていない」というリンターのチェックを黙らせつつ、
2                // コメントでは「ここでは戻りを見る必要はない」などと書いておく。

これに近い解は、今の場合はないんだよね。_data_migration での例のように「必ず指示せよ、でないと海に飛び込むから!」てのもダメなわけだから。(もちろん「無理やり意味のない代入などをする」のは今度は別の警告をトリガーするハメになるので、どんなステートメントを書いても役には立たない。)

というわけで、「設定で特例を作れないか」ということになる。一応 argsIgnorePattern が「近い解」ではあるんだけれど、そのものズバリではないのが痒過ぎる。これについてまさに issues#5086 で不満を持っている人がいることがわかる。が現状出来ることはここまで。まぁ仕方ないか…。ともあれ非常に気持ちが悪いワタシの .eslintrc.js:

 1 module.exports = {
 2     "env": {
 3         "browser": true,
 4         "jquery": true,
 5         "es6": true
 6     },
 7     // ...
 8     "extends": "eslint:recommended",
 9     "parserOptions": {
10         "sourceType": "module",
11         "ecmaVersion": 2017
12     },
13     "rules": {
14         // ...
15         "no-unused-vars": [
16             "error",
17             {
18                 "args": "after-used",
19                 "argsIgnorePattern": "(^force$|^_|_$)",
20                 "ignoreRestSiblings": true,
21             }
22         ],
23         // ...
24     },
25 }

アンダースコア始まり、アンダースコア終わりは普通は「使うつもりはない」の表明に近いので無視しつつ、_update_elements_style 「であろうがなかろうが」、「force という引数ならばチェックを怠れ」という指定。全然やりたいこととは違うが、まぁ「一時的にやむなく」ということでやむなし、と考えるしかない。(issues#5086 で作者が sorry と言いつつ閉めてるそのまんまの話。)