ESLint: no-param-reassign

またこういうことを言うとアレだ。

no-param-reassign ルールなのだが、その名前とは裏腹に、実際には2つの異なる問題をテリトリーとしている。公式の例より:

いか~ん、いか~ん、いか~ん…
1 function foo(bar) {
2     bar = 13;
3 }
いか~んずら、いか~んずら、いか~んずら…
1 function foo(bar) {
2     bar.prop = "value";
3 }

見かけは同じことに思えるかもしれないし、ほかの言語未経験者には違いを説明出来ないかもしれない。

上の例は「浮気しないでね」、下の例は「いつまでもそのままでいてね」、簡単に言えばこの違いである。前者は要するに「参照相手の不変表明」、後者は要するに「オブジェクトの不変表明」(immutable)の話。


浮気しないでね、に関して、「関数の引数ではなくて、オブジェクトが元々不変で、なおかつ java」だと結構話はわかりやすい:

1 final String s = "aaa";
2 if (複雑な何か条件) {
3     // ...
4     // ...何か複雑な処理...
5     // ...
6     s = "bbb"; // ノー!!
7 }

final にしたかったわけだから、何かの理由があって “aaa” から浮気したくなかったのであろう。めでたくこの “bbb” へのアサインは java コンパイラによって拒絶される。とりわけ複雑な処理を書く必要がある場合に、この final は非常に便利だ。誤って「意図と違うこと」を書かずに済む。

同じ浮気しないでね、でも、以下は少し違う:

 1 class MyPrivate {
 2     private int _hoge;
 3     public void setHoge(int v) {
 4         this._hoge = v;
 5     }
 6 };
 7 // ...
 8     final MyPrivate p = arg;
 9     if (複雑な何か条件) {
10         // ...
11         // ...何か複雑な処理...
12         // ...
13         p.setHoge(200); // 全く不当ではないし、それが意図なら「間違いですらない」
14     }

つまり final が保障してくれるのはあくまでも「参照相手を変えることが出来ない」ということであって、オブジェクトの不変要求とは違う。

そしてこのことこそが、「多くの java 初心者を傷付けてきた」わけだが、言ってしまえばこれは「ポインタを徹底的に悪者扱いした java」の宿命である。(一応言っとくと java にはポインタがないので安全だ、という誤ったことが言われるが、実態は違う。「C で言うポインタ相当のものしかない」というのが正解。そして真の問題は、「そうであるのに、そうでないと強硬に主張するために発生した歪み」である。)


対して C 系の言語の場合は、というか「C++ に触発されたのちの C 以降の C 系の言語は(要するに C89 以降)」、「いつまでもそのままでいてね」を直接コンパイラに指示することが出来る。何が違うの、と混乱するかもしれないが、全然違う。簡単に言えば、「class (等) の作者自身による不変表明とは無関係に不変要求することが出来る。つまり、class の利用者自身が「これは読み取り専用として使いたい」ということを指示できる、ということ。まぁ要するにこれは const という「役割過多な」キーワードで指定するわけだが、java の final と「同じ使い方にも違う使い方にも」使える。つまり「浮気したくない」const と、「変わってくれるなよ」の const、書き方は違うが同じ const で可能。

なんでこんな致命的な差が生まれるかと言えば、フライングで言及したとおりで、「指すものと指されるもの」という概念を強く意識しているかどうか、の差である。例えばこれである:

C++
 1 #include <string>
 2 struct MyClass {
 3     int x;
 4     int y;
 5     std::string name;
 6     MyClass(const std::string& name) {
 7         this->name = name;
 8     }
 9 };
10 int main()
11 {
12     MyClass* const mc1 = new MyClass("佐藤");  // あなたを一生愛し続けます…、サトゥーさん
13     const MyClass* mc2 = new MyClass("後藤");  // あなたは一生ゴトゥーさんのままです…
14 
15     //mc1 = new MyClass("近藤"); // ゴメン、好きな人が出来ちゃった (許さん! by コンパイラ)
16     //mc2 = new MyClass("近藤"); // ゴメン、好きな人が出来ちゃった (ええよ、別に。by コンパイラ)
17     //mc2->x = 100; // ゴトゥーさん、変わっちゃったのね (許さん! by コンパイラ)
18     return 0;
19 }

「ポインタ」というものが、元々が「オブジェクトを指し示すもの」という概念であるために、C/C++ は元々が「指すもの・指されるもの」について意識的であり、プログラマにもそれを意識させるように仕向けている。すなわち、「どこを指したいんだい?」と問うそのままの延長線上に、「それをどう指したいんだい?」を一緒に意識させる。実生活で何になぞらえればわかりやすいかと言えば、「触れることなく指差す」か「手掴みして良いか」の差。あるいはレーザポインタと指し棒の違いになぞらえてもいい。(要は主体が「あなた」である、ということですよ。「指し棒で対象に触りたいかどうか」を、あなた自身が選べ、ということ。)

そして。「ポインタなんか害悪しかもたらさない、そんなもの永遠になくなってしまえ、だから C はダメなんだ!」という C/C++ への誹謗中傷からスタートしている java では、「ポインタがもたらすもの」そのものに否定的でなければならないわけだから、「指すものと指されるものの区別」そのものを「プログラマに出来るだけ意識させないように」デザインされている。すなわち、java でも仮にこういうことが出来れば良かったのである:

1 class MyPrivate {
2     private int _hoge;
3     public void setHoge(int v) {
4         this._hoge = v;
5     }
6 };
7 // ...
8     final MyPrivate x1 = new MyPrivate(); // 一生あなたを愛し続けます…
9     MyPrivate final x1 = new MyPrivate(); // あなたは一生貴様のままでいやがれ…

これを java が発明しなかったのも無論「指すもの・指されるもの」という関係を希薄に見せる必要があったからである。すなわち、「変数・オブジェクト・代入」という誰もが理解出来る言葉だけで説明したい、というわけだ。試しにこの3つの用語だけで「どう使いたい」の表明を言語に追加することを想像してみ? 「変数に、オブジェクトを、代入します」のどこかに入れるんだぞ? どうしたって「指す・指さない」の概念が紛れ込んでくるはずである。

現状の final は「変わらない変数に、オブジェクトを、代入します」ということ。「変わらないオブジェクトを」が可能だって? そりゃぁ無理である。必要な概念は「変わらないオブジェクト」ではないんだから。「利用者が変える気がない」なのだよ。この「利用者が」というのがポイントで、ここにどうしたって「指す」という概念が必要。すなわち、「参照である」であるとか、「オブジェクトを指し示す」という、「複雑で難解で初心者泣かせの C/C++ の醜い言語仕様」から来る用語を徹底的に嫌ったがために、「使われ方は選べるが使い方は選べない」というなんともおかしな歪みが生まれてしまった、ということなのである。

だいいち java が攻撃するのは「C/C++ の(ある意味においての)複雑さ」なので、「final の位置によって意味が違う!」なんてのは、設計ポリシーからして許されない。const というキーワードを増やすことも、おそらく同じような理由で拒絶されたのであろう。

従って java では、この手のことをしたければ、「使い手側が意識する指す・指されるの関係」とは違うところで実現する必要がある。すなわち、「ワタシ、ゴトゥーは、永遠に不滅です」という具合に、class の設計者自身が「不変」を実現するしかない。利用者がゴツーに頼むことは出来ないのである。具体的なとこだと例えばこんな具合か:

1 public class Aaa {
2     private final String name;
3     public void Aaa(final String name) {
4         this.name = name;
5     }
6 }

このように Aaa の作者が不変であるように書けば、「ゴチュー」はゴチューであり続けられる。


ここまでがいわゆる「引数でない場合」のおよそ基本的な話である。

引数の場合も結局はこの「指す・指される」の関係性は踏襲されるのだが、もう一つ、「コピーを渡すのか、参照(あるいはポインタ)を渡すのか」すなわち技術用語では「値渡しと参照渡し」の差がある。C は理論的には「実体渡ししかない」が、「ポインタの実体を渡す」ことによって、参照渡しと同じことを実現する。C++ は C の仕様を引き継ぎつつも、参照渡しも出来る。java は参照渡ししか出来ない。(ちなみに Fortran や COBOL などもいわゆる参照渡ししかない。むしろ C の方が異端。)

すなわち C++ の以下:

1 int myfunc(SomeClass s)
2 {
3     s.hoge = 1;
4 }

これは、myfunc は「SomeClass オブジェクト」を受け取るわけであるが、呼び出し側が渡したもの「のコピー」が渡ってくる。なんですと?? そう、これこそが「C++ で初心者泣かせ」の仕様として名高い仕様であり、なおかつ C++ の場合は、「スライス」の問題がある。スライシングが何かについては…、そうね、Advanced C++ とか Effective C++ を読めば書いてあるので、そっち読んだらいい。ゆえ、要するに「大抵は参照渡し(相当のもの)にしなければならない」ということになり、普通はこうする:

1 int myfunc(SomeClass& s)
2 {
3     s.hoge = 1;
4 }
5 // あるいは (C++的にはあまり推奨出来ない)
6 //int myfunc(SomeClass* s)
7 //{
8 //    s->hoge = 1;
9 //}

対して、参照渡ししかない java では、こういったダークサイドがないので「ほらみたことか、C/C++ だせー、java まんせー」てわけね。


さて、そろそろ元々の話、「ESLint: no-param-reassign」の話に少しずつ入っていこう。長かったね、すまん。けどまだ続く。

あまりに長かったので、発端の ESLint の「NG」を再掲:

いか~ん、いか~ん、いか~ん…
1 function foo(bar) {
2     bar = 13;
3 }
いか~んずら、いか~んずら、いか~んずら…
1 function foo(bar) {
2     bar.prop = "value";
3 }

このことを、C++ と java で先に考えておく。

C++ の場合は実際は「最初からそう設計しておく限りは」、実に簡単な話でもある:

そもそもプリミティブ型の参照を受け取ろうとするアホさは今は問わない。
1 void foo(const int& bar)
2 {
3     bar = 13;
4 }

いかーんもくそもなくて、「ワタシはあんたがくれたそれを大事にするワ」と宣言する限り、そもそもこれは「コンパイラが守ってくれる」。これはいわば「(foo の作者が foo の利用者に)あなたがくれるそれを守ります」という風に、利用者に誓約を表明するわけである。だからつまりは、もし C++ でこれと同種の警告を喰らうのだとしたら、間違いなくインターフェイスに何か問題を抱えている。C++ の場合はそもそもインターフェイスでほとんど解決出来る問題なのである。

先の値渡しの件も忘れずに触れておこう(これがのちの話に効いてくる):

1 void foo(int x)
2 {
3     for (int i = 0; i < 10; ++i) {
4         ++x;
5     }
6     return x;
7 }

これの何かが悪なのだとしたら、「初心者が foo 呼び出し元の x を書き換えてくれると期待してしまう可能性がある」ということである。つまり、

 1 #include <iostream>
 2 int foo(int x)
 3 {
 4     for (int i = 0; i < 10; ++i) {
 5         ++x;
 6     }
 7     return x;
 8 }
 9 int main()
10 {
11     int myx = 10;
12     foo(myx); // 10足してくれるんでしょ??
13     std::cout << myx << std::endl;  // なんでだぁ!!
14 }

これを「なんて日だ!!」と騒ぎ立てるならば、「C++ は罠だらけの最悪の言語、すわ、スタイル標準ねば!!!」という思想になる。けれども、よほどの「バカ」でない限り、こんなことで何か問題を起こすようなプログラミングをしてしまうようなエンジニアなぞ、皆無である。皆この事実を共有している。(C/C++ でそうしたい場合、つまり引数の中身を書き換えたい場合は参照もしくはポインタで受け取るインターフェイスにするわけだが、そうすれば java と同じ悩みを抱えることが出来る。)

対して java である。先に「参照渡ししかない」と言った通りで、しかも「class 利用者が不変表明は出来ない」ことを考えた場合、以下は C++ 利用者には非常に残念なお知らせになる:

1     //...
2     public void somefun(final String s) {
3         // ...
4     }

いいだろうか。渡ってくるのは「参照」である。すなわち、「参照へのリアサイン」は、「普通はいつだって意図するものと違う」のである。これは丁度 C の値渡しの例と実は同じなのだ。final を付けなけりゃいいんでしょ、なこれ:

1     //...
2     public void somefun(String s) {
3         // ...
4         s = "bbb";
5     }

これは単に「渡ってきた参照とは別のものを指す(浮気する)」という行為でしかなく、かつまた、final を付けることは、「それを許されない」というだけのことであって、実に「クライアントコードには全然関係ない」。単に「somefun の実装者が滅多に起こさない特定の誤りを犯さない」ためだけに付ける final で、クライアントコードに対しては何にも表明しない。言っただろ? 「変わらずにいてね」と指示することは元々出来ないのだから。すなわち、

1     //...
2     public void somefun(final MyClass ms) {
3         // ...
4         ms.set_hoge("bbb");
5     }

は、相変わらず(MyClass が禁止しない限りは)相変わらず可能だし間違いでないし、それが間違いだとするならば防ぐ手段も(MyClass が頑張らない限りは)ない。要するに java で引数に final を付けるのは「非常に稀なミスを微かに救う」ためだけに使う。

さて、いい加減元の ESLint の話である。

javascript は C++ よりは java の方に近いわけだが、「参照渡し」だろうか、「値渡し」だろうか? 答えは「参照渡し」である。ESLint 公式の例を実際にやってみればすぐにわかる。

つまり、java が抱える問題をそのまま抱えている、ということになる。しかも「final」だとか「const」は(たぶん)使えないので、「あなたを守ります」という C++ 的な表明は元から出来ない無法地帯、ということになる。だけれども「そうしたいのならば」、つまり「引数内容を書き換えたいのだ」ということならば、もちろんそれが仕様なのだから、「禁止される言われはない」というのが本当のところだ。

ここまではいいかな?

しかるに、

いか~ん、いか~ん、いか~ん…
1 function foo(bar) {
2     bar = 13;
3 }
いか~んずら、いか~んずら、いか~んずら…
1 function foo(bar) {
2     bar.prop = "value";
3 }

これらを「いか~~~~~~~ん」というのは実際は何を言っているのか? 無論、「参照渡しであることをあてにして、中身を書き換えようとする発想そのもの」を NG とする、ということである。必要であれば内容をコピーしてでも「戻りで返せ」ということ。

no-param-reassign が禁止する一つ目のパターンは、「C/C++ の値渡しの例と同じ初心者の混乱」:

 1 function foo(x) {
 2     var i;
 3     for (i = 0; i < 10; ++i) {
 4         ++x;
 5     }
 6     return x;
 7 }
 8 function main() {
 9     var myx = 10;
10     foo(myx); // 10足してくれるんでしょ?
11     console.log(myx); // なんでだぁ!!
12 }

これを「有害だ!」とする主張だろう。あとで言うが、「ほんとか?」という意見もある。

no-param-reassign が禁止する二つ目のパターンが java でずっと説明してきた「参照渡しでしかない」ことから来るそのものの混乱(?):

1 function foo(someobj) {
2     someobj.x = 10;
3 }
4 function main() {
5     var myx = new MyObject();
6     foo(myx); // myxはオレのもんだ
7     console.log(myx); // なんで変わってるんだぁ!!
8 }

これは「初心者向けに危うい」ということも意味するが、これを禁止するに足る根拠はむしろ「プログラム全体が制御しにくくなる」ということであり、なので「多分 93.56% くらい」は、このルール適用は有用だ。けど「必ずそうすべし」という無条件ルールにするのは危うい。けれども「なぜ適用対象外なのか」の説明が、これもこの話に通ずるところがあって「説明しずらい」よなぁ。つまり「複雑で大きなオブジェクトを常にコピーせよ」には従えないこともある、てことだ。


実際のところどうなんだ、について、正直「props」(つまりこれまで見てきた「後者」)の方はやってもいいかな、と思うのだが、「前者」つまり参照のリアサインについては、正直「いやぁ、それ禁止されてもなぁ、嬉しぅない」と思うのである。ワタシのコードがまさにそれをしててな:

 1 MalUrlBuilder.prototype.get_image = function(relative_url, no_proxy) {
 2     var _this = this;
 3     if (!relative_url) {
 4         relative_url = _this._UNK_IMAGE_URL;
 5     }
 6     if (relative_url.startsWith(_this._PROXY)) {
 7         return no_proxy ? relative_url.slice(_this._PROXY.length) : relative_url;
 8     }
 9     if (relative_url.startsWith("https://") || relative_url.startsWith("http://")) {
10         return (no_proxy ? "" : _this._PROXY) + relative_url;
11     }
12     return (no_proxy ? "" : _this._PROXY) + _this._BASE_IMGURL + relative_url;
13 };

目茶目茶普通の発想でもって relative_url について「浮気」しているが、これは当たり前であろう、「渡してくれない」という「あなた」から浮気したいんだから。(undefined 相手に浮気しているのである。)

確か java の checkstyle にも非常に似たノリのチェックがあって、C/C++ 脳には理解出来なかった記憶が強い。つまり C/C++ 熟練者が「コピーで渡ってくるものを書き換えて何が悪い」と考えるのが普通だし、「java や javascript のパラメータリアサイン」がそれと本質的に完全に同じであることを知っている限り、「何か問題でも?」と思うのは当たり前。現実のプログラミングでは例えば「入力データに補正を加えてから本題の処理をする」というようなことをしたいケースで頻発するニーズである。これを「害悪だ」と言われれば「そうかもしれない」。つまり「本物の入力データ」を(意図して/意図せず)失う、とした場合に、「意図せず失う可能性があるなんて万死に値する!!」、まぁ言わんとすることはわからいでない。けど「そこまでなのか?」と思う、ということである。だいたいこういうのって、「一人の開発者がその開発者人生において一度やらかすかやらかさないか」ほどの稀なことを「大げさに誰もが陥る大きすぎる罠である!」と喧伝されることが多く、実際今話題にしてるこれも、結構それに近い。初めて学習するのに「一回地獄に(0.1秒だけ)落ちる」が真相だったりするんだわ、こういうのって。

簡単に言えば「misleading を避ける」ことに主眼があるのがこうしたチェックなのだが、本当のところは「呼び出す側に作用するかどうか」だけを根拠に考えた方がいいのである。つまり、「前者」の例が、「呼び出し側の環境を壊さない」ということをまず「知れ」ということ。「誤解を招くなんて悪だ!」よりも何よりも先に、「呼び出し側と呼び出される側の相互作用」を「常識」として共有する方が先だろ、というスタンスに立つならば、「誤解するほうが悪」だ、この場合。

最終的にこの no-param-reassign に対して何が言いたいか? 無論「主従を逆にしてくれ、ワシは props の方しかチェックしとうない」てことだ。

props の方は「意図してやってる箇所」では特例として適用除外と考えるが、結構「間違ってやってしまう」ことがあるのだ、だから普段はチェックしておきたい。つまり、「触らないつもりなのに誤って更新してしまう」ミスを C++ では「const MyClass&」で受けることによって防げるが javascript ではこれが出来ないので、ESLint に期待したい、というわけだ。けれども props でない方はハッキリいって「意図してわかってそうしてる」ので、こんなもんで不平を言われるのはたまらん。


はぁ、言いたかったのはこれだけなんだが、えらい長くなっちゃったね。すまんすまん。


「なんか java にだけ厳しくない? Python だって一緒じゃないの?」。すまん、その通りである。言っちゃ悪いが、java を恨んでいない C++ エンジニアなんか一人もいない。Python は C/C++ を攻撃なんてしない。むしろ仲良くやろうとする。無論 Perl も Ruby も皆同じである。java だけが「後戻り出来ないほどに C/C++ コミュニティを破壊した」のだ。だから文句を言う権利がある。(そして念のために言っとくが、ワタシは本業での 6年ほどの java 開発経験者だ。だから好きなところはもちろんあるし、その価値も知っている。)