React is React, just.

2024年2月17日

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

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

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

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

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


ref:

Footnotes

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

関連コンテンツ

記事一覧に戻る