ゆめみ23卒アドカレの24日目(遅刻)です!
きっかけ
以下の記事で自分好みのResult
型をつくりました!というのを見て、筆者としても思うところがあったので実装して公開してみました。
筆者は普段フロントエンドを中心に活動しているため、元記事や既存のライブラリと異なる設計になったため、布教も兼ねて記事にしました。
きっかけの記事
自作したもの
筆者がResult型への思いを語った記事
パッケージ公開するまで
筆者がResult型ライブラリに求めていたこと
以下では敢えて、フロントエンドをユーザに直接作用する領域
、バックエンドをユーザに隠蔽された領域
、クライアントサイドとサーバサイドをコードが実行される領域(ブラウザとサーバ)
として明確に区別します。
- 現代のフロントエンドで利用できること
- もちろんバックエンドでも使えます
- 関数合成が容易なこと
1. 現代のフロントエンドで利用できること
2023年現在、一周回ってフロントエンドはクライアントサイドとサーバサイドの両方を活用し、レンダリングを最適化する方向にシフトしています。Next.js
やRemix
、Qwik City
はこの方向性で新機能の追加、開発されています。しかし、ブラウザとサーバという異なる実行環境でコードを実行する都合、いくつかの制約が発生します。
- ブラウザ、サーバ間の通信にはシリアライズが必須であること
- ブラウザからサーバのコードを実行する処理が複雑になりやすい
2つ目については、上記のフレームワークがある程度解消しています。Next.jsのApp Router
とServer Actions
、Qwik CityのrouteLoader$
とrouteAction$
などが挙げられます。これらの機能をうまく使えば、型情報を保持したままクライアントとサーバを横断した処理を記述することが可能です。
しかし、1つ目のシリアライズ周りは現状解決されていないと言ってもいいでしょう。上記の機能を活用しても、クライエント/サーバ間はシリアライズされたデータが受け渡しされることには変わりなく、メソッドなどシリアライズ出来ないプロパティを持つ値を正しくやりとりできません。superjsonなどをうまく活用すれば、不可能ではないでしょうが。ほぼ間違いなくハマるポイントが増える上、余計なコードがクライアントサイドに紛れ込む可能性があるため、この対応は避けたいところです。
また本番環境において、Server Actionsでエラーはマスクされ、クライアントサイドで受け取ることは出来ません。シークレットや実行環境の情報流出の可能性など、機密的に仕方ないところもありますが、クライアントでサーバサイドのエラーに触れられないことで不便に感じる場面もあるでしょう。
Result
型に埋め込む値自体はライブラリの使用者に委ねる他ありませんが、Result
型自体がシリアライズ不可であれば、そもそもクライアントサイドとサーバサイドを超えることが出来ません。
Result
型の実装はクラスを避けて、プレーンなオブジェクトとそれを操作する関数郡で実装する必要があります。
2. 関数合成が容易なこと
筆者はHaskellの関数合成やF#のパイプラインが好みであり、fp-ts
やremeda
のpipe
関数を利用してコードを記述することが多いです。配列はremedaのmap
関数でpipe
関数向けに記述可能ですが、Result
型がメソッドチェーンのみでは中途半端な記述になってしまいます…
メソッドを実行する度にv => v.method()
のようなラムダ関数(JSに置いてはアロー関数)を定義したり、ラッパー関数を実装するのは避けたいところです。そのため、ある程度pipe
関数を前提した実装にしたいところです。
既存ライブラリはどう?
実装する前に上記を両方満たすライブラリを探してみたのですが、筆者の探す範囲では両方見たすものはありませんでした… クラスによる実装だったり、関数がカリー化されていないなど…
ということで勉強も兼ねて自分で実装してみることにしました。
実装方針
これらを踏まえて以下のような実装方針になりました。
- Result型自体はプレーンなオブジェクトで表現する
- 実行環境に依存しない関数群でResult型の処理を行う
- イミュータブルに操作する
- 手続き的な関数と関数型的な関数を両方実装する
3つ目に関しては正格評価と遅延評価(厳密には異なるかも?remedaにおける遅延評価と同じ)と言い換えることも出来ます。
現在は一部の関数を、正格評価@totto2727/eager
と遅延評価@totto2727/lazy
に分割して実装しています。将来的にはremedaのように1関数に統合することも視野に入れていますが、現状は型定義の複雑性を省くためにも分割しています。
また、Result型の命名はSwiftを参考にしています。RustなどはJavaScriptのネイティブErrorと命名が似ており混同する恐れがあるため、明確に分割しています。
副次的なメリット
基本的な方針とメリットはすでにしましたが、この実装により副次的な利点もあります。
ツリーシェイキングが容易であること
コードを分割できないクラスとは異なり、細かい関数で実装されているため、不要なコードを削除しやすくなっています。最近のフロントエンドフレームワークならページ単位で使用しない関数をオミットできるはずです。もともとのコード量も少ない(ESMでは5kB未満)ため、数kBで利用できるようになりました。
デメリット
一方でこの方針による避けられないデメリットも存在します。
メソッドチェーンが使用できない
手続き的な記述も可能なようにしていますが、一般的にOOPで用いられる(特にサーバサイドJSに多い?)メソッドチェーンは全て関数で実装している関係上一切機能しません(値にアクセスするだけ)。適宜関数をimportする、もしくは事前にまとめて再エクスポートする必要があります。
エラーの値に注意する必要がある
これは本ライブラリに限った問題ではありませんが、機密情報がクライアントサイドに流出しないよう注意する必要があります。本ライブラリではTypedCause型など、型付きで例外処理するための型もあるため、直接例外のオブジェクトをResult型に含めるのではなく、適切な例外通知用オブジェクトに積み替えることを推奨します。
あらゆるコードがResult型に汚染される
これも本ライブラリに限らず、さらに言えばPromiseなどにも通ずる問題です。
筆者としては、いわゆるUIライブラリ(ReactやVueなど)の領域、特に状態やPropsとしてResult型を用いることは推奨しません。外部APIへのフェッチからstateに保存する過程では積極的に利用することを推奨しますが、コンポーネントに渡す段階でResult型を変換する、もしくはunwrapでネイティブの例外を投げましょう。
ネイティブの例外機構が好きになれなかったことも作成のモチベーションの一つです。しかし、フレームワークによっては自動で例外をキャッチして、適切に処理する機構もあるため、明示的にその機構を利用するべき場面ではむしろ積極的にunwrapしましょう。
しかし、コアなビジネスロジックはフレームワークに依存しない形が望ましいと考えられるため、どこまでResultで扱い、どこからはunwrapを許可するのかはプロジェクトごとに定める必要があるでしょう。
これはフロントエンド、サーバサイド問わず、調整が必要な部分になります。
最後に
本記事は以上になります。Result
型への思いからパッケージの作り方、本記事と気づけばこのライブラリ関係で3記事も公開していました。個人的にはとりあえずフロントエンド、バックエンドを気にせず利用できるライブラリが完成したので満足しています。
始めて公開したライブラリですが使ってもらえると筆者が喜びます。
バグや機能要望などあれば、IssueもしくはPRをねげていただけると幸いです。