🎂

TS のパターンマッチングもゼロランタイムの時代に? pattycake を試してみた

2023/10/02に公開

X(旧 Twitter)のタイムラインを眺めていたら、以下のポストが目に入りました。

https://twitter.com/aidenybai/status/1708267583961731532

これは pattycake という名のツールで、ts-pattern(パターンマッチングを提供する有名なライブラリ)のコードを最適化してゼロランタイム化できるとのこと。
面白そうなので少し触ってみました。

https://github.com/aidenybai/pattycake

試したコード

まずは Vite の vanilla-ts テンプレートを使用してプロジェクトを作成し、その後 ts-patternpattycake をインストール。

npm create vite@latest try-pattycake -- --template vanilla-ts
npm try-pattycake
npm install ts-pattern pattycake

Result 型っぽいデータを返す関数を作成し、

src/getResult.ts
type UserResponse =
  | { type: 'ok'; data: { name: string } }
  | { type: 'error'; error: { message: string } }

/** 20% の確率でエラーを返す関数 */
function getUser(): UserResponse {
  return Math.random() > 0.2
    ? { type: 'ok', data: { name: 'Alice' } }
    : { type: 'error', error: { message: 'Oops! Something went wrong.' } }
}

その結果をパターンマッチして文字列を返す処理を作りました。[1]

src/getResult.ts
export function getResultText(): string {
  const response = getUser()

  return match(response)
    .with({ type: 'ok' }, (res) => res.data.name)
    .with({ type: 'error' }, (res) => res.error.message)
    .exhaustive()
}

それを DOM から呼び出すようにして準備完了。

<button id="button" type="button">push</button>
<p>result: <span id="result"></span></p>
src/main.ts
document
  .querySelector<HTMLButtonElement>('#button')!
  .addEventListener('click', () => {
    const text = getResultText()
    document.querySelector('#result')!.textContent = text
  })


ボタンを押すとレスポンスが表示される

ビルド結果

いったんこの状態(pattycake なし)でビルドしてみます。
生成されるコードが読みやすいように、ミニファイは無効化しておきます。

npx vite build --minify false

出力された JS ファイルは 9.05 kB で、中身をみると 227 行あり、そのうちの 160 行ほどが ts-pattern 由来のコードでした。

pattycake 導入

pattycake を有効化するには README にある通り、vite.config.js で Vite プラグインを追加します。

vite.config.js
import { defineConfig } from 'vite'
import pattycake from 'pattycake'

export default defineConfig({
  plugins: [pattycake.vite()],
})

再度ビルドしてみると、JS ファイルのサイズは 2.21 kB で、中身は 81 行にまで減っていました。
ts-pattern を使っていた部分は、以下のような if 文を含んだ即時関数式に変換されていました。

dist/assets/index-{hash}.js
function getResultText() {
  const response = getUser();
  return (() => {
    if ((response == null ? void 0 : response.type) === "ok") {
      let res = response;
      return res.data.name;
    }
    if ((response == null ? void 0 : response.type) === "error") {
      let res = response;
      return res.error.message;
    }
  })();
}

比較用に変換前のコードを再掲。

src/getResult.ts
export function getResultText(): string {
  const response = getUser()

  return match(response)
    .with({ type: 'ok' }, (res) => res.data.name)
    .with({ type: 'error' }, (res) => res.error.message)
    .exhaustive()
}

たしかに、同等のコードに変換されていますね。

ここまでをまとめると以下のようになります。

ファイルサイズ 行数
pattycake なし 9.05 kB 227 行
pattycake あり 2.21 kB 81 行

ランタイムパフォーマンスはどうなのか

pattycake の README に「ts-pattern は素晴らしいが、代償として桁違いに遅くなる」との記述(意訳)があるので、ランタイムのパフォーマンスも比較してみます。

ボタンのイベントハンドラー内に、以下の 10 万回のループを追加。

src/main.ts
console.time()
for (let i = 0; i < 100_000; i++) {
  getResultText()
}
console.timeEnd()

pattycake なし・ありの状態でそれぞれ 10 回ずつ実行した中央値がこちら。

実行時間
pattycake なし 9.82 ms
pattycake あり 2.09 ms

数字の上では確かにだいぶ高速化していますが、10 万回実行してたった 7ms の差なので、ほとんど誤差レベルな気がします。複雑なパターンマッチを大量に使用したとしても、おそらく体感速度に響くほどの差は出なさそうです。

おわりに

冒頭に書いた通り、まだ "highly experimental software" であり、ts-pattern のすべての API に対応しているわけではないので使用には注意が必要です。
最近は主に CSS-in-JS などでゼロランタイムのライブラリが増えてきていますが、「他のライブラリのコードをゼロランタイム化する」というコンセプトは珍しい気がしました。


今回試したコードはこちらのリポジトリにあります。
https://github.com/jay-es/try-pattycake

脚注
  1. コールバックの引数部分を ({ data }) => のように分割代入で書きたかったのですが、現時点の pattycake では未対応でした。 ↩︎

Discussion