ログイン新規登録

気に入った記事にいいねで気持ちを伝えませんか?💚

いいねした記事はマイページのいいねした記事一覧からいつでも読み返せます

88

1行で発狂するコード

投稿日

人は1行の記述で死ぬこともある

公開からコンスタントに閲覧をいただいている以下の記事。需要がありそうなので、もう1つ記事を書いてみようと思う。

題材は前回同様のJavaScriptのクソコードではあるが、前回は冗長を極めたクソコードであるのに対し、今回はたった1行である。

1行なのに、なんと示唆深い(決してほめていない)コードだろうか。

不可思議なコード

では、実際に見てみよう。

return a = a >= 10 ? 10 : a >= 5 ? 5 : a >= 3 ? 3 : a >= 2 ? 2 : 1, z * a;

これを見て、何が return で返されるかわかります?

代入演算子

最初見たとき、代入文 a = ××× の評価値って何になるのかな? areturn されるのかなと思いました。

割り当て操作は、割り当てられた値として評価されます。

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行は、結果的に学びにはなった。だからといって感謝はしていない。

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
  3. ダークテーマを利用できます
ログインすると使える機能について
jaque

@jaque

読み:ジャック(あるいは ハケ)
この記事は以下の記事からリンクされています

コメント

fujitanozomu
@fujitanozomu(藤田 望)

そうそう、別に a に代入する(a に書き戻す)必要はないよね。

さっさとreturnしちゃうのが良いんじゃないですかね。

if(a >= 10) {
    return 10 * z;
}
if(a >= 5) {
    return 5 * z;
}
if(a >= 3) {
    return 3 * z;
}
if(a >= 2) {
    return 2 * z;
}
return z;
8
www-tacos
@www-tacos

おもしろさの中に学び(カンマ演算子)もあって良い記事でした:laughing:

個人的には、ネストする三項演算子は改行とインデントが必須だと思っています。
https://qiita.com/yuba/items/9832c26dc1498e0017bb

なので今回のコードもいい感じに整形したら意外とありかも?と思って書いてみましたが、やっぱりダメですね。(再代入とカンマ演算子がksすぎる)

return (
  a = a >= 10 ? 10
    : a >= 5  ? 5
    : a >= 3  ? 3
    : a >= 2  ? 2
              : 1
), z * a;
6
jaque
@jaque
(編集済み)

@fujitanozomu さん
おっしゃる通り、私もすぐに返すでいいかなとも思いました。
私なら最後の return は、1倍であることを明示するために、以下のように書きたいですかね。書き忘れだと思っちゃいそうだし・・

return 1 * z;

@www-tacos さん
果敢なチャレンジ、ありがとうございます:muscle:
そもそもreturn の後ろにカンマ演算子を置くのが、アンチパターンですね。
個人的には、インデントはVSCodeからフォーマッタに任せたいので三項演算子は1行で書き、記事にある通り、三項演算子のまとまりをカッコで括りたい派です。
それで見づらいようならif文を使ってしまうかも。

2
so_asa
@so_asa

minifyされたコードではこのカンマの使い方は基本的なテクニックですね。知っているとプロダクションビルドされたコードが読みやすくなります。

5
otn
@otn

returnなのにaに代入してる時点で「後ろのほうで参照するんだな。これは心して読まねば」と察するべきということでしょうね

1
Nabetani
@Nabetani(鍋谷 武典)
(編集済み)

私としては、左が大きい不等号つらい、不要な再代入は避けたい、一行が長い、括弧があったほうがいい、という印象は持ちましたが、それほどわかりにくいとは感じませんでした。(個人の感想です)

初見で苦なく理解できました。倍率の計算とかですかね。

条件演算子の部分は左から順に読めばわかるので高難易度とは思いませんでした。
難易度が高いと感じるのは下記のようなものです。

return a = a>=3 ? 10<=a ? 10 : 5>a ? 3 : 5 : a<2 ? 1 : 2, z*a;

それと a がローカル変数であれば

return z * (a >= 10 ? )

で済むはずなのに、わざわざ

return a = a >= 10 ? , z * a

としているので、return 後も a の値が活きるのかもしれないと想像していまして、もしそうなら、そのための代入が return 内で行われている点がひどいと感じます。

蛇足ですが、PHP は三項演算子の優先順位が違う(ことがある?)ので要注意です。

参考になれば幸いです。

0
albireo
@albireo

このコンマ演算子はC由来のもので、式しか書けない場所に複数の処理を書いたりするのに使われてました

Cで,が使われる典型例
for(i=0, j=99; i<100; i++, j--) {
}

でもCの文法をベースにしているjavaやC#が採用しなかったように、問題点も認識されていました。
javascriptがそんな,演算子を採用したのは意図的なものと言ってよさそうなので、考え方の違いを感じられる箇所ですね。

@otn さん

returnなのにaに代入してる時点で「後ろのほうで参照するんだな。これは心して読まねば」と察するべきということでしょうね
return a=<aに入れたい値>,<値を返したい式>
は単純に
a=<aに入れたい値>;return <値を返したい式>
と置き換え可能で文字数も変わりません。
そのためこういうのは意図的に紛らわしくしてる難読化や嫌がらせと考えていいと思います。

「returnの右辺に代入文」の実用的な使われ方として思い浮かぶのは「返値を変数にも保存しておきたい」を一行で済ませるといったケースですね。

function GetAccessTime(){
  //最後に取得した時刻を覚えておいて、経過時間の計算などに使用する
  return lastAccessTime = new Date();
}
0
i_tatte
@i_tatte(いたて)

たぶんこのコードを書いた人は関数型プログラミング言語を書いてきた人なんでしょうね。

let f z = let a = 略 in z * a;;

みたいな処理をそのまま直訳して

function f(z) {
    return a = 略, z * a;
}

と書いてしまったような感じがします。関数型として見ればかなり自然な書き方なので。

4
belq
@belq

仮にこれをレビューする立場になったら、
可読性/メンテナンス性が低いので冗長な記述に変えてください
くらいしか浮かばなかったけど(主観だから心のなかで反論されそう)
デバッグしにくいってのもあるのか

0
makoto-developer
@makoto-developer(makoto-developer)

これをASTに変換するとどういう式になるんだ。冗長なコードとあまり変わらない?

0
D-Three
@D-Three

これは...何をしたいかなんとなくわかるけど、もっとこう...あるだろうってなりますね。

let f z a =
    [ 10; 5; 3; 2 ]
    |> List.tryFind (fun t -> a >= t)
    |> Option.defaultValue 1
    |> fun t -> a * t * z
0
ashworth
@ashworth(ash worth)

頭がまともならこうじゃねぇの?
あまりにもバカらしいので動くかどうか検証してないけど。
あれかな? 掛け算には順序があります!とか言ってる脳みそウニな小学校算数信奉者なんかな?
なんでreturnするだけでカンマがどうしたいう話になるんやろ?

return z * (
		(a >= 10) 
			? 10 
			: (a >= 5) 
				? 5 
				: (a >= 3) 
					? 3 
					: (a >= 2) 
						? 2 
						: 1
	);
0
ashworth
@ashworth(ash worth)

https://qiita.com/jaque/items/b99ed9dce78cc64fa9d2#comment-df9cc431b87878f1d3dc
これ、酷いなぁ…。
こんなのにいいねが6もつくんや…。
世も末やなぁ…。

0
ashworth
@ashworth(ash worth)

分かったようなこといってるけど、

さっさとreturnしちゃうのが良いんじゃないですかね。
https://qiita.com/jaque/items/b99ed9dce78cc64fa9d2#comment-b04a114fe519eb0f19e0

変数定義したら負けなんか?君らの知能だと…。
頭痛くなってくるわ…。

0
ashworth
@ashworth(ash worth)

必要もないカンマ演算子とやらを使っただけで
「おー!興味深いですねぇー!!!」
とか盛り上がるから
世間一般からは『プログラマはキモい』って思われてるし、
全く信頼されてないんよなぁ…。

「エンジニア不足です」って言われてる“エンジニア”は、
そいうキモオタの事じゃないねんけどなぁ…。

0
ashworth
@ashworth(ash worth)
(編集済み)

はぁ!?

スクリーンショット 2024-03-08 232249.png

何を言ってんねん…。頭痛くなってくるわ…。

上の文章と下の文章で整合性取れてないやろ。
分けるべきが結論なら何に代入しようが関係ないやん。

ほんま、力抜けるわ…。

0

いいね以上の気持ちはコメントで

記事投稿キャンペーン開催中
Qiita×Findy記事投稿キャンペーン 「自分のエンジニアとしてのキャリアを振り返ろう!」
~
88

新規登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる

ソーシャルアカウントでログイン・新規登録

メールアドレスでログイン・新規登録