sosukesuzuki.dev

November, 13 2023

【翻訳】Prettier の新しい三項演算子のフォーマットを試してみてください

この記事はAlex Rattrayさんの A curious case of the ternaries を、本人の許可を得て翻訳したものです(タイトルは大幅に変えてしまっていますが)。

記事の最後にあるように Google Forms から新しい機能についてのフィードバックを求めています。私以外のメンテナーも読めるようにできるだけ英語で書いてほしいですが、「日本語でなら書いてもいいよ」という人がいたら日本語で書いてもらっても大丈夫です。


三項演算子のフォーマットは長年の課題でした。Prettier の v3.1.0 では新しいフォーマットのスタイルを導入することで、ついにこれを解決しました(訳注: 後述の通り、まだ experimental なので、--experimental-ternaries をつけたときのみ有効になります)。

このブログ記事では、これまでの経緯と背景、実際に触ってくれた開発者からのフィードバックと、新しい「curious ternaries」スタイルの概要について説明します。

--experimental-ternaries オプションを試してみて、ご意見をお聞かせください。

簡単な概要については v3.1.0 のリリースブログを参照してください。

導入

ネストされた三項演算子を様々な状況で適切にフォーマットすることは、驚くほど難しい課題です。

開発者はそういった三項演算子を非常に読みにくいと感じていました。そのため最終的に、コードの一部を見苦しい if 文と let 変数を使ってリファクタリングしたり、即時実行関数や完全に別の関数に切り出したりすることもありました。

ベータ版を実際に使ってくれた人たちによると、私たちが開発した新しいフォーマットスタイルは、慣れるまでに時間がかかるかもしれませんが、最終的には、最新のコードベースで三項演算子を使えるようになるでしょう。

歴史的な背景

Prettier のもともとの単純なアプローチ(ネストされた三項演算子の各レベルにインデントを追加するだけです)は、単純な場合には上手く機能しました。しかし、ネストされた三項演算子の長いチェーンには対応できていませんでしたし、他にもいくつか問題もありました。

そこで 2018 年に、これをインデントをしないフラットな三項演算子に置き換えました。これは当時は良いアイデアのように思われましたが、評判はあまり良くありませんでした。元に戻すように求める issue には 500 を超える upvote が寄せられました。

今回リリースした v3.1.0 ではインデントされた三項演算子に戻しましたが、より良い方法を見つけたいと考えていました。

ここ数年の間私たちは、一般的なケースではインデントされた三項演算子と同じくらい読みやすくて、さらに様々な状況で適切に機能するようなソリューションを、いくつも考えて実験してきました。

挑戦的な基準

理想的には、私たちは以下の基準を満たすスキームを 1 つ見つけ出したいと考えています:

  1. すべての場合において、どの部分が if なのか、どの部分 then なのか、どの部分が else なのかを簡単に把握することができる必要がある。
  2. 単一の三項演算子から、2 つの三項演算子のチェーン、さらにネストされた長いチェーンまで、ちゃんとスケールできる必要がある(私たちが検討した代替案のほとんどはこの条件を満たすことができませんでした)
  3. JSX、TypeScript の conditional types(これは if では表せません)、および通常の JavaScript のすべてを同じように扱える必要がある
  4. 三項演算子のネストがどれだけ長くてもスケールできる必要がある(数十にネストされた TypeScript の conditional types を想像してみてください)。

v3.1.0 で帰ってきたシンプルなインデントありの三項演算子は、明らかに 4 を満たしません。さらにおそらく 1 や 3 も満たすことができません。また、フラットで読みやすい形式の JSX の三項演算子もサポートしていましたが、JSX 以外では不自然な形になっていました。

多くの人たちは Rust や OCaml などの言語の match の構文からインスピレーションを得て、単純な「case style」が良いのではないかと話していましたが、2 や他の目標を満たすことはできませんでした。

訳注:

「case style」というのは、条件と同じ行に、その条件の then 部分を書き、最後の行に残った else 部分を置くスタイルを指すようです。簡単に言うと次のコードのようなスタイルです:

const message =
  i % 3 === 0 && i % 5 === 0 ? "fizzbuzz" : 
  i % 3 === 0 ? "fizz" : 
  i % 5 === 0 ? "buzz" :
  String(i);

長くネストされた三項演算子全体に対して「case style」を適用すると読みにくくなってしまうというのが著者の主張です。訳注終わり。

驚くべき解決策

幸いなことに、私たちの基準を満たすフォーマットスタイルのアルゴリズムが見つかりました。しかし残念ながら、ほとんど開発者にとっては馴染みのない斬新なものになってしまいました。

この機能のベータテストでは、このスタイルを見た開発者が、最初は懐疑的な姿勢を取ることがわかりました:

しかし、少し使ったあと、彼らは元には戻りたくないと言い始めたのです:

さらに、別の開発者は次のように述べています:

ルールを有効にしてから最初の 1 時間は、少し奇妙だと思いました。しかし、2 時間目までには、汚い if へのリファクタリングを回避できました。もう元のスタイルには戻れません。

私はネストされた三項演算子が嫌いでしたが、コードを if - else 文にリファクタリングするのも嫌いでした。この新しいフォーマットのルールは理解しやすくて、まるで if 式を言語に追加するようなものです。

これらのフィードバックを得て、私たちは winning formula を手に入れたと感じました。しかし、それをいきなりデフォルトで有効にすると、コミュニティにとっては不快であることもわかっていました。

そのため、私たちはこの新しいフォーマットを数ヶ月の間 --experimental-ternaries オプションとして提供し、それまでの間はコミュニティが求めていたもの、つまりインデントされた三項演算子のフォーマットを提供することにしました。

スタイルの概要

では、この新しいスタイルはどのようなものでしょうか?

「curious」三項演算子の背後にある考え方を示す簡単な、やや不自然な例を以下に示します:

const animalName =
  pet.canBark() ?
    pet.isScary() ?
      'wolf'
    : 'dog'
  : pet.canMeow() ?
    'cat'
  : 'probably a bunny';
  1. ? で終わるすべての行は "if" を表します
    • もし foo ? を見つけたら、それは foo について "if foo? then, ..." と質問するようなものです
  2. : で始まるすべての行は "else" を表します
    • もし : foo を見つけたら、それは "else, foo" を意味します
    • もし : foo ? を見つけたら、それは "else, if foo" を意味します
  3. :? もないすべての行は "then" を表します
    • もしただの foo を見つけたら、それは "then foo" を意味します

そして、「case style」の三項演算子の例を示すために書き換えた例を以下に示します:

const animalName =
  pet.isScary() ? 'wolf'
  : pet.canBark() ? 'dog'
  : pet.canMeow() ? 'cat'
  : 'probably a bunny';

新しいフォーマットのスタイルは「curious」な三項演算子と、「case style」の三項演算子(? が行の中央にある)をなめらかに組み合わせたものです。

例を示します:

const animalName =
  pet.canSqueak() ? 'mouse'
  : pet.canBark() ?
    pet.isScary() ?
      'wolf'
    : 'dog'
  : pet.canMeow() ? 'cat'
  : pet.canSqueak() ? 'mouse'
  : 'probably a bunny';

フィードバックをください!

新しくて読みやすいデフォルトを気に入っていただけるとうれしいです。また、--experimental-ternaries を有効にして、数週間試してもらえるとうれしいです!

実際に使ってみて、Google Forms からフィードバックをください: https://forms.gle/vwEuboCobTVhEkt66