ryokatsu.dev

メモ:proposal-pattern-matchingのプロポーザルを読んだ


OCmalやHaskellにはあるパターンマッチング構文が、JavaScriptにも提案があったのでプロポーザルを読んだ。あるのは知っていたけどちゃんとプロポーザル読んでなかったので備忘を残す。

※2023年8月時点でステージ1なのでこれから大きく仕様が変更される可能性がある

課題とモチベーション

JSには、switchがあるが、フォールスルー(breakを忘れて次のcase文が実行されてしまう)スコープが曖昧などの問題があるので、もう少し人間の読みやすいように機能がほしいということで生まれた提案。switchで提供できない機能を導入したいということらしい。

サンプルコード

いきなりコードをみた方が良いと思うので、プロポーザルにあるものをそのまま抜粋

match (res) {
  when ({ status: 200, body, ...rest }): handleData(body, rest)
  when ({ status, destination: url }) if (300 <= status && status < 400):
    handleRedirect(url)
  when ({ status: 500 }) if (!this.hasRetried): do {
    retry(req);
    this.hasRetried = true;
  }
  default: throwSomething();
}

match式をブロック全体に囲いwhen()でマッチさせたいものを書く。これはどんな式でもOK。when句では、左辺と右辺の間にコロンがあり左辺のマッチに成功したら右辺が評価されてmatch全体の値となってくれる。default句もある。

do式とのコンビネーション

右辺に関しては、任意の式を入れることができるが複数のステートメントを入れることを想定しており、別の提案でdo式がある。これが入ると以下のようなことが実現できる

const res = await fetch(jsonService)
match (res) {
  when ({ status: 200, headers: { 'Content-Length': s } }):
    console.log(`size is ${s}`);
  when ({ status: 404 }):
    console.log('JSON not found');
  when ({ status }) if (status >= 400): do {
    throw new RequestError(res);
  }
};

そもそもこのdo式はめっちゃ欲しい機能の1つでReactのJSXで三項演算子によるコンポーネントの出し分けとか、{if (props.isXXX) {<isXXX /> else {<XXX />}}}みたいな書き方をしなくて良くなる。

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

そして今回の、pattern-matchingをあわせるとこうなる(プロポーザルから抜粋)

<Fetch url={API_URL}>
  {props => match (props) {
    when ({ loading }): <Loading />
    when ({ error }): do {
      console.err("something bad happened");
      <Error error={error} />
    }
    when ({ data }): <Page data={data} />
  }}
</Fetch>

これはAPIのデータによってそれぞれコンポーネントを出し分けしている例だけど、結構いい感じ。ただAPIのフォールバックの書き方は、ReactだとSuspenseとかあるからそっちに方が、良いのかもしれない。

備忘録

  • 先行技術として、ts-patternがある。これはめちゃくちゃ良くて、おそらく使っている人も多いのではないか。あとDenoからもoptionalsというやつがあって、Result型とかを簡単にimportできるっぽい。

  • 配列の長さは、どうやら暗黙的にチェックするらしい(そりゃそうか)配列のマッチ方法は、結構色々ある。詳しくはここにある。反復可能な場合は、[Symbol.iterator]を使える。

  • 正規表現のパターンマッチは、結構使えそうな感じがしている。

  • 面白いものとして(${})という書き方がある。テンプレート文字列に似ているとうか分かりづらいけど、カスタムマッチャーSymbol.matcherというものがあり、名前付きのメソッドを持ったオブジェクトとして解決される場合は、メソッドを呼び出すことができ、それ以外は、プリミティブな値として解決していてパターンマッチングしてくれるみたい。コードを読むと何となくわかる

class MyClass = {
  static [Symbol.matcher](matchable) {
    return {
      matched: matchable === 3,
      value: { a: 1, b: { c: 2 } },
    };
  }
};

match (3) {
  when (${MyClass}): true; // matches, doesn’t use the result
  when (${MyClass} with {a, b: {c}}): do {
    // passes the custom matcher,
    // then further applies an object pattern to the result’s value
    assert(a === 1);
    assert(c === 2);
  }
}
  • コンビネーターとしてorandがある。
    • orは、入れ子になったパターンのどれかにマッチすれば成功
    • andは、入れ子になったパターンのすべてにマッチすれば成功
  • 構文に優先順位がないため、同じ入れ子の中でwhen ("foo" or "bar" and val)みたいな書き方をするとエラーになる。
    • when ("foo" or ("bar" and val))という感じで書く必要がある
  • 将来的に機能として入れる可能性のあるものの中で、チェーンガードなるものがあった。これは結構欲しいかも

感想

  • これが入るとJSのパラダイムシフトが起こりそうな機能ではある。書き方が結構変わりそう。
  • でも個人的にはめっちゃ使いたい。
  • 割りとこの機能を実装する側(各ブラウザ)は大変そうなイメージがあるので、いつになることやら。。。
  • ひとまずdoだけでもShipされてほしい