Twitterで散らかしてしまったので軽くまとめておく.

序・前提

まず前提として,コンパイラを使った最適化を行うという方針については私はとても賛成である. インターフェースを崩さずにDXを改善するにあたってこのアプローチはしばしば有効的であるし,私自身もそれを全面に押し出すフレームワークを使っている. ただし,コンパイラが介入するにあたってのメンタルモデルの変更(または統一 [^1])についていくつかの疑問がある. 私は普段からReactを書いているわけでもなく専門家でもないので,これから話すことはReactに対する意見というより,「Reactを使っている人は,この点どう感じているのだろうか?」という好奇心からくるもので,その是非に対するものではない. 何度も言うが私は 賛成 している.

また,これらはhttps://react.dev/blogでReact Compilerについての話が出た時に感じたことで,これまでReact Compilerについて行われてきた議論などをしっかり調べたりして根拠を作ったものではない. ただのエッセイだ.

私が持っている疑問に対するアンサーは歓迎しているが,このエッセイを根拠にしたReact CompilerやReact自体の是非については歓迎しない. 技術に対する是非はこの一部を切り取って評価できるものではない. (と思っているので,もしこのエッセイにそう捉えられかねない発言があれば,ぜひ指摘してほしい.(是非について何か広めたいわけではない))

私の立場: 感想・疑問をまとめておく 読み手の立場: 感想・疑問について議論・返答する やらないこと: 技術に対する是非

なぜわざわざここまで保険をかけたかというと,自分が叩かれたくなかった,とかそういう意味ではなく, パラダイムに関する是非は視点によって大きく変わるものであり,それについて話すには議論が足りてなさすぎるからだ. 技術の是非に関して,誤謬が広まってしまうことは本当に良くないことだと思っているので,それは避けられたい.

トピック

まずここからすでに主観の話になるが,Reactは「純粋なJavaScript」であるという主張が多かれ少なかれあるはずだ. ここで,「純粋」とは何かについては多義であると感じているので,注意が必要だと思っている.

純粋とは: (ⅰ) React Componentは冪等なJavaScriptである (ⅱ) React ComponentはJust JavaScriptである

今回,特に私が気になっていたのが,「React Compilerの登場で (ⅱ) を満たせなくなってしまったみたいだが,どう感じているのか?」という点だ. 前提として,React Compiler登場以前はどちらも満たせていたという主張のもとである.

(ⅰ) に関しては,React Compilerはむしろ「コンポーネントは冪等であるべき」というルールの元の最適化実装なのでReact Compiler登場後も当然そう言える.この,(ⅰ) で考えている意味論のことを「React的意味論」と呼ぶようにする.

問題は (ⅱ) だ.React CompilerはReact的意味論は変化させないが,「JS的意味論は変化させている」という主張だ. ここについて,Reactを使っているの人たちがどう感じているのかについて興味がある.というのが今回のトピックだ.


「JS的意味論は変化させている」というのがどういう意味なのかについて.

以下のようなコンポーネントを考えてみよう.

これは実際React Advanced 2023のReact Fogetについての登壇で登場したコードだ.(一部省略)

function VideoTag({ heading, video, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }

  if (filteredVideos.length === 0) {
    return <NoVideos />;
  }

  return (
    <>
      <Heading
        heading={heading}
        count={filteredVideos.length}
      />
      <VideoList videos={filterdVideos}>
    </>
  )
}

このようなコードを見た時,Reactユーザーならば不要な計算に気づき,最適化を行うことだろう.

function VideoTag({ heading, video, filter }) {
  const filteredVideos = useMemo(() => {
    const filteredVideos = [];
    for (const video of videos) {
      if (applyFilter(video, filter)) {
        filteredVideos.push(video);
      }
    }
    return filteredVideos;
  }, [videos, filters])


  if (filteredVideos.length === 0) {
    return <NoVideos />;
  }

  return ...;
}

参考: Understanding Idiomatic React – Joe Savona, Mofei Zhang, React Advanced 2023 t=196

しかし,React Compilerの登場によってこれが大きく変わりそうだ. React Compilerは,このコード(useMemoを使っていない方)をコンパイラが解析し,メモ化を行うJavaScriptコード に変換する.

function VideoTab(t36) {
  const $ = useMemoCache(12);
  const { heading, videos, filter } = t36;
  let filteredVideos;

  if ($[0] !== videos || $[1] !== filter) {
    filteredVideos = [];

    for (const video of videos) {
      if (applyFilter(video, filter)) {
        filteredVideos.push(video);
      }
    }
    $[0] = videos;
    $[1] = filter;
    $[2] = filteredVideos;

  } else {
    filteredVideos = $[2];
  }

  if (filteredVideos.length === 0) {
    return <NoVideos />;
  }

  return ...;
}

参考: Understanding Idiomatic React – Joe Savona, Mofei Zhang, React Advanced 2023 (t=497)

これはつまり,「React的意味論におけるJavaScriptコード(出力コード)の最適化」であり,「JavaScriptの意味論」自体は変化している.

JavaScriptの関数というものは,

function Func(n) {
  let list = [];
  for (let i = 0; i < n; i++) {
    list.push(i);
  }
  return list;
}

とかくと,その結果に関わらず必ず実行されるものだ.

だが,React Compiler登場後のReact Componentはそうではない. 見かけ上はJS関数だが,Just JS関数ではなくなってしまった と言えそうだ.

function VideoTag({ heading, video, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }

 ...
}

と記述したはずなのに,関数の呼び出し回数と実際に記述した処理の回数は一致しない. このような評価規則はJavaScriptにはないもので,React CompilerがReact的意味論を元に最適化した結果だ.

これで,「React Compilerの登場によって,React ComponentがJust JavaScriptではなくなってしまう」というところが伝わっただろうか.


意味論は2つに増えたが,構文論は1つである.

実際に,ReactでComponentを実装する際に「React的意味論」と「JS的意味論」の2つを併用するべきかという議論はさておき,

function VideoTag({ heading, video, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }

 ...
}

この関数を見たときに, 2つの意味論が存在してしまうことは事実だ. (React的意味論と,JS的意味論) あるときは,関数はの呼び出し回数とbodyの実行回数は一致するかもしれないし,あるときはしないかもしれない. これが開発者や学習者にとって負担になるかどうかは分からないが,そういった可能性はゼロではないはずだ.

構文論に関する利点について,注意したいこと

今までは主に,

function VideoTag({ heading, video, filter }) {
  const filteredVideos = [];
  for (const video of videos) {
    if (applyFilter(video, filter)) {
      filteredVideos.push(video);
    }
  }

 ...
}

という関数に対する意味論について取り上げてきたが,構文論についてだ. 構文論については,React Compilerの登場以前/後に限らず紛れもなく「Just JavaScript」だ. (JSXはそもそもJSではないだろという話もあるが,今回の話の本筋ではないので割愛する)

ReactがJust JavaScriptであるという利点の一つとして,静的解析の容易さやツールチェーンとのインテグレーションのしやすがしばしば挙げられる. これらに関しては引き続きそう語って概ね良い範囲だと個人的には思われる. (だからと言って,他のものがダメであるかどうかはまた別の議論) 厳密にはJS的意味論が重要なツールなどでは部分的にそうではなくなる可能性もあるが,感覚的にはそこが重要になってしまうようなものはほとんどない気がしている (完全に主観).

しかし,注意したいのはその切り分けである. React Compiler登場以前,「ReactはJust JavaScriptだから ${任意の評価}」は意味論と構文論のどちらにも適用されることがあった.しかしReact Compilerの登場後はそれは構文論の話になったのではないかと思う.

「ReactはJust JavaScriptだから ${任意の評価}」と言われた時,この区別がないと間違った評価をしてしまうことがありそうだ,という考えが私にはある.これがどの程度問題になるかは定かではいが,新たに生まれた問題提起なのではないかな,と思った.

[^1]: もちろん,中にはもともとReact的意味論のみを考えていた方もいると思う.その方にとっては特に変わりがないが,JS的意味論も視野に入れていた人はReact的意味論のみを考えるように統一されていくものなのかな,と思った (実際どうなのかはわからない)

つまりReact Componentは何であると言えそうか

JavaScriptの意味論を変えてしまっては,「Just JavaScript」とは言えなさそうだ. ではなんなのだろうか? ・ ・ ・ ・ Reactは,Reactだ. 構文 がJavaScriptと同じで,意味が異なる別の存在だ.

と思ったが皆さんはどうだろうか... 🤔

エッセイ: ReactはJavaScript + Ruleなのか,それとも言語なのか

https://x.com/ubugeeei/status/1758298301609492584?s=20


<details> <summary>ref:</summary>

https://x.com/ubugeeei/status/1758296154406821942?s=20

https://x.com/ubugeeei/status/1758308229602529304?s=20

https://x.com/ubugeeei/status/1758309937300808122?s=20

https://x.com/ubugeeei/status/1758458714749911445?s=20

https://x.com/ubugeeei/status/1758467330278035779?s=20

https://x.com/ubugeeei/status/1758508131385266543?s=20

https://x.com/ubugeeei/status/1758521001091191294?s=20

</details>