人は1行の記述で死ぬこともある
公開からコンスタントに閲覧をいただいている以下の記事。需要がありそうなので、もう1つ記事を書いてみようと思う。
題材は前回同様のJavaScriptのクソコードではあるが、前回は冗長を極めたクソコードであるのに対し、今回はたった1行である。
1行なのに、なんと示唆深いに富む(決してほめていない)コードだろうか。
不可思議なコード
では、実際に見てみよう。
return a = a >= 10 ? 10 : a >= 5 ? 5 : a >= 3 ? 3 : a >= 2 ? 2 : 1, z * a;
これを見て、何が return
で返されるかわかります?
代入演算子
最初見たとき、代入文 a = ×××
の評価値って何になるのかな? a
が return
されるのかなと思いました。
割り当て操作は、割り当てられた値として評価されます。
JavaScriptの仕様を確認し、「return a = ×××
は、a
がローカル変数である限り、 return ×××
ということか、冗長だな」と判断しました。
しかし、このコードは a
を返すことはありません。
閑話休題(?) 三項演算子
この1行の大半を占める三項演算子 ? :
は、もはや筆者には見慣れたものだ。規約で使うなといわれることが多いが、私はそれほど抵抗はない。
しかし、演算子の優先順位が重要なこの例示の1行では、 ?
の前の条件式部分には、せめて ()
で括って意味のまとまりを見せておきたいかな?
(a >= 10) ? 10 : (a >= 5) ? 5 : ………
三項演算子のネストがひどいのではないかという読者も多いだろうが、筆者はこれを見たときにはそんなに感情は動かなくなった。a
を対象として比較する一貫性があるので、まだ見られるコード。
カンマ演算子(コンマ演算子)
この行の最後は、1, z * a
で終わる。私は当初、すべての三項演算子で 偽 と判定されたら、z * a
が返されるのかなと思っていた。カンマ演算子の仕様を見てみよう。
それぞれの演算対象を(左から右に)評価し、最後のオペランドの値を返します。
ただ、筆者の理解は間違っていた。このカンマ演算子は、優先順位が最低位の演算子であることを見落としていた。しかも、前方の 代入演算子 =
よりも後に評価される。
つまり、1, z * a
をカタマリとして理解してはダメで、最後のオペランドとしては z * a
のみであり、return
される対象は、return
から一番遠い、z * a
なのだ。
,
の前の a = a >= 10 ? 10 : a >= 5 ? 5 : a >= 3 ? 3 : a >= 2 ? 2 : 1
は、返す値を決めるために計算しているだけだったのだ。
いみじくもWikipediaを見ていると、以下のようにある。
左被演算子を評価しその値を捨て、その後右被演算子を評価する演算子である。
だいぶ後ろに記述されたカンマ演算子ごときでその値が捨てられるのである。頭から机上デバッグしていたレビューアーは、「捨てんのかい!」とツッコミを入れずにはいられない。
1行にすることの罪
1行で何でもしようとすると、デバッガでブレークポイントを置きづらいのも苛立つポイントである。デバッグ目的で2行以上に変更すればブレークポイントも貼れるのだが、改行によって処理が変わらないか確認するのも難しい。
処理の順序
前述の不可思議なコードを書き直してみよう。
if(a >= 10) {
a = 10;
}
else if(a >= 5) {
a = 5;
}
else if(a >= 3) {
a = 3;
}
else if(a >= 2) {
a = 2;
}
else {
a = 1;
}
return z * a;
上記は、解釈しやすいように記述しているので、行数でいえば冗長かもしれない。ただし、return
で何が返ってくるのはz * a
とわかりやすくなった。
せめて下に示すぐらいには分けられなかったのかね。
const b = ((a >= 10) ? 10 : (a >= 5) ? 5 : (a >= 3) ? 3 : (a >= 2) ? 2 : 1);
return z * b;
そうそう、別に a
に代入する(a
に書き戻す)必要はないよね。
まとめと学び
- 代入文の評価値は、代入した値である
- 三項演算子はネストもできる
- カンマ演算子の評価値は、最も右にあるオペランドである
- 可読性の観点から1行書く限度がある
この1行は、結果的に学びにはなった。だからといって感謝はしていない。