DTPab

DTPにまつわるあれこれ

ExtendScriptにおける三項演算子の不具合への検証と対策

2021-11-02追記
あるふぁ(仮)さんからこの記事の間違いについてご指摘いただきました。

実際、やってみると本当にこの通りの挙動です。
これ以上ExtendScriptの入れ子の三項演算子について深入りすると帰ってこられなくなりそうなのと、これによって僕が挙げた結論については揺るがないので、再検証まではしないでおきます(が記事は残しておきます)。
あるふぁ(仮)さん、改めていつもありがとうございます。
追記ここまで

昨日だったか、id:haraguai_is_bad さんが興味深い記事を書かれていました。

haraguai-is-bad.hatenablog.com

この件について僕なりに考えてみました。

結論

先に結論だけ書いておきます。

  • 三項演算子は使わないが吉(可読性も悪くなるため)
  • 三項演算子を入れ子で使う場合は、処理範囲を明示するようにグルーピングを行う

これを導いた検証が下記です。お遊びみたいなものなので話半分でお付き合いください。あくまで私見ですので間違いなどがあればご指摘いただけると嬉しいです。

おさらい

数学の四則演算でいえば、乗算・除算が加算・減算よりも先に行われ、それらを左から順に演算子を挟んだ左辺と右辺とで計算を行います。

8 + 12 / 4

なら、先に 12 / 4 が計算され、答えは 11 です。
優先順位を変えるには ( ) でくくればいいので、先に 8 + 12 を計算するためには

( 8 + 12 ) / 4

とすると、答えは 5 になります。

このような演算子ごとの解決の優先順位がJavaScriptでも明確に決まっています。
JSでの演算子の優先順位はMDNを参照してください。
算数でいうイコール(=)は、JSでは代入演算子=でこれも演算子のひとつですし、プロパティアクセサーである.も演算子です。これらを優先順にしたがって解決していくわけです。

結合性

前掲のMDNのページに、演算子の結合性(結合規則)についても記述があります。

詳細はリンク先を参照してもらいたいのですが、かいつまんでいうと、例えば

a = b = 5;

という式があったとき、結果はaにもbにも5が代入されます。この式はbに5を代入→abの内容を代入した結果*1です。つまり右側の演算子から先に計算(解決)されたわけです。同じ優先順位の演算子があったとき、右側の演算子から解決するか、左から解決するか、というのがこの結合性です。

先のMDNのページの一覧表で、結合性の欄に「右から左」とあるのがこのような計算順序になるもので、三項演算子は「右から左」に結合性をもちます。

以上までがなんとなく理解できたら、本題の三項演算子について考えます。

与件

最初に紹介したブログ記事にあった下記について検証したいと思います。

var x = 30;
var zz = (x < 10) ? 'aa' : (x < 50) ? 'bb' : (x < 90) ? 'cc' : 'dd';

本来の正しい挙動

変数zzには'bb'が入るのが正しい挙動です。順に追って見ていきます。

まず最も優先順位の高い( )の内側を解決します。変数xには数値型の30が入っていますので、結果はこうなります。

false ? 'aa' : true ? 'bb' : true ? 'cc' : 'dd';

三項演算子の結合性は右から左なので、最初にtrue ? 'cc' : 'dd'を解決します。

false ? 'aa' : true ? 'bb' : 'cc';

続いてtrue ? 'bb' : 'cc'が解決されて…

false ? 'aa' : 'bb';

最終的に'bb'が式の評価となるはずです。

ExtendScriptの間違い

同じものをExntedScriptで評価させると結果は'cc'となります。どういう計算順序で計算が行われているのか、仮説を立ててみました。

三項演算子が左側から解決されている?

前述の( )を解決した時点で、式の中には三項演算子しかありません。これが左から右に解決されるとどうなるでしょうか。

false ? 'aa' : true ? 'bb' : true ? 'cc' : 'dd';

これを左から順に解決すると…

true ? 'bb' : true ? 'cc' : 'dd';

となるため、結果は'bb'とならなければおかしいです。やはり結合性の通り、右から左に解決されることは間違いなさそうです。

三項演算子の左辺と右辺のくくり方がおかしい?

ひょっとして、こんなふうに考えたのかな? というのが今回の仮説です。

(false ? 'aa' : true ? 'bb' : true) ? 'cc' : 'dd';

三項演算子の?より左を全部左辺にするという乱暴なくくり方です。そして( )の中の三項演算子?も同様に考えることにすると、

( (false ? 'aa' : true) ? 'bb' : true) ? 'cc' : 'dd';

ということになり、これを順に解決(内側の( )が最優先)すると

(true ? 'bb' : true) ? 'cc' : 'dd';
'bb' ? 'cc' : 'dd';

となり、'bb'はtruthyですから、結果が'cc'となります。

検証

では、与件の'bb'の部分をfalsyにすれば、結果が'dd'になるはず。

var zz = (x < 10) ? 'aa' : (x < 50) ? 0 : (x < 90) ? 'cc' : 'dd';

'bb'のところを0にしてみて結果を見てみると、予想通り、見事'dd'となりました。

正しい順序で計算させるには

適宜グルーピングを行うしかないでしょう。入れ子の三項演算子がそれぞれどの範囲で評価すべきかを右側から順に明示するようにグルーピングします。
与件の式であれば、

var zz = ((x < 10) ? 'aa' : ((x < 50) ? 'bb' : ((x < 90) ? 'cc' : 'dd')));

このようにします。この式の結果は正しく'bb'となります。

最後に

結論としては冒頭に書いたとおりです。入れ子の三項演算子は可読性が悪く、truthyやfalsyといった式の評価としての基本的なことが理解できていないと予期せぬ結果になりかねません。個人的にはおすすめしない書き方ですが、haraguai_is_badさんが書いているように、これが使われているコードを読むために必要な知識でもあります。
「きれいで正しいコード」という明確なものはないですが、ひとつの基準を考えるとすればやはり「誰が見ても読みやすいコード」なのかなと思います。読みやすいコードとはつまりコードを読んで結果を想定しやすい=予期せぬ不具合が起きにくい、そしてそれは式の評価を曖昧にしないコードなのかなと思うのです。

以上、三項演算子についての検証でした。

*1:もし左側から解決される場合、aにbの値が代入されたあと、bに5が代入されるはずですが、結果はそうはなりません