TS のパターンマッチングもゼロランタイムの時代に? pattycake を試してみた
X(旧 Twitter)のタイムラインを眺めていたら、以下のポストが目に入りました。
これは pattycake という名のツールで、ts-pattern(パターンマッチングを提供する有名なライブラリ)のコードを最適化してゼロランタイム化できるとのこと。
面白そうなので少し触ってみました。
試したコード
まずは Vite の vanilla-ts テンプレートを使用してプロジェクトを作成し、その後 ts-pattern
と pattycake
をインストール。
npm create vite@latest try-pattycake -- --template vanilla-ts
npm try-pattycake
npm install ts-pattern pattycake
Result 型っぽいデータを返す関数を作成し、
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]
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>
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 プラグインを追加します。
import { defineConfig } from 'vite'
import pattycake from 'pattycake'
export default defineConfig({
plugins: [pattycake.vite()],
})
再度ビルドしてみると、JS ファイルのサイズは 2.21 kB で、中身は 81 行にまで減っていました。
ts-pattern
を使っていた部分は、以下のような if 文を含んだ即時関数式に変換されていました。
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;
}
})();
}
比較用に変換前のコードを再掲。
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 万回のループを追加。
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 などでゼロランタイムのライブラリが増えてきていますが、「他のライブラリのコードをゼロランタイム化する」というコンセプトは珍しい気がしました。
今回試したコードはこちらのリポジトリにあります。
-
コールバックの引数部分を
({ data }) =>
のように分割代入で書きたかったのですが、現時点の pattycake では未対応でした。 ↩︎
Discussion