FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

TypeScriptでもEffectを使ってScalaのZIO並に最高にハイな開発体験🚀

こんにちは!ScalaではZIOでしか書きたくないと思っている菅野です。
この記事はFLINTERSブログ祭りの記事です。テーマは #技術 です。

今回はタイトルの通り、EffectというTypeScriptのライブラリを紹介しようと思います!

Effectとは

effect.website

Effectとは簡単に言うと、最強で最高のTypeScript用のライブラリです。
エラーハンドリングから非同期処理、依存管理に至るまで開発者が直面するあらゆる課題を解決するためのシステムをワンストップで提供します。

EffectはScalaのZIOのようなものなのですが、Scalaを知らない人にもこのライブラリ良さを紹介したいので、難しいことは考えずに便利な機能をただ列挙しようと思います!

モナドの話なんて理解しなくてもドキュメントのとおりに書くだけでとりあえず便利に使えるんじゃないかなと勝手に思ってます。

便利な機能

コンテキストを扱えるロガー

JavaScriptのライブラリにはコンテキストを扱えるログライブラリがあまり見つからないのですが、Effectにはその機能を持ったログ機構があります。
annotateLogsを使うことで特定の範囲に追加の情報をもたせることが出来ます。
下記のコードでは独自のフォーマットのロガーを作って出力しています。

import {Cause, Effect, LogLevel, Logger} from "effect";
import {logLayer} from "./logger";

const task1 = Effect.gen(function* () {
  yield* Effect.logDebug("task1 done").pipe(Effect.annotateLogs("result", "success"));
});

const task2 = Effect.gen(function* () {
  yield* Effect.logDebug("task2 done");
});

const program = Effect.gen(function* () {
  yield* Effect.logInfo("start");
  yield* task1;
  yield* task2;
  yield* Effect.logError("done", Cause.fail(new Error("boom!")));
})
  .pipe(Effect.annotateLogs("requestId", "123"))
  .pipe(Logger.withMinimumLogLevel(LogLevel.Debug));

Effect.runPromise(Effect.provide(program, logLayer));

コード全体がなんか妙な書き方ですが、慣れると始めからこういう言語だったかのように思えてきます!大丈夫です!*1*2

"task1 done"のログにはresultとrequestIdの両方が出力されています。

{"timestamp":"2024-06-04T10:40:13.769+09:00","severity":"INFO","message":"start","attributes":{"requestId":"123"}}
{"timestamp":"2024-06-04T10:40:13.790+09:00","severity":"DEBUG","message":"task1 done","attributes":{"result":"success","requestId":"123"}}
{"timestamp":"2024-06-04T10:40:13.790+09:00","severity":"DEBUG","message":"task2 done","attributes":{"requestId":"123"}}
{"timestamp":"2024-06-04T10:40:13.790+09:00","severity":"ERROR","message":"done","stack_trace":"Error: boom!\n    at next (/Users/k_kanno/repos/test_effect/src/main.ts:16:45)","attributes":{"requestId":"123"}}

欠点はスタックトレースの起点がnext(ジェネレーター関数から生成されたイテレーターのnextメソッド)になること。
どのコンテキストで呼ばれたか知りたい場合はログに出るようにするのが重要です。

依存管理

Effectは依存管理をすることができます。
つまりはDIコンテナの代わりです。

JavaScriptではコンストラクタインジェクションやデコレーターを使ったDIコンテナで依存の分離と管理をすることが多いですが、Effectには依存管理の仕組みがあるので更に別ライブラリを入れたりコンストラクタ引数の修正地獄になることはありません!

コード例が長くなってしまうので、詳しくはManaging Services – Effect Docsを参照してください。
ちなみに先程のログ出力のコードではEffect.provide(program, logLayer)でログの実装を指定しています。

この依存管理はEffectではよく使う重要な機能です。
コードの分離やテスト時のモック差し替えなどに利用できます。

型安全でスマートなエラーハンドリング

Effect<Success, Error, Requirements>

Effectの型定義は上記の通りで、3つの型パラメータは成功時の返り値の型エラー時のエラー型実行に必要な依存です。
どんなエラーが発生するかを型で表明できるので、より堅牢なエラー処理ができます。

また、Eitherというデータ型が用意されています。
これは実行の成功(Right)か失敗(Left)かを表す型で、処理の結果によってEitherはRightかLeftのどちらかの値を取ります。

これとEffectを組み合わせると、どこか一つが失敗したら全体が失敗するという処理を透過的に書くことが出来ます。
下記のコードでは2個目のEitherがleftになっていて、自動的に全体が失敗したとしてEffectの結果はエラーになります。

const p: Effect.Effect<number, Error, never>= Effect.gen(function* () {
    const x = yield* Either.right(1);
    const y = yield* Either.left(new Error("boom!"));
    const z = yield* Either.right(3);
    return x + y + z;
})
Effect.runPromise(p).then(console.log);
Error: boom!
    at next (/Users/k_kanno/repos/test_effect/src/main.ts:28:34)


また、TaggedErrorで型にreadonly _tag = タグ名が付いたエラー型を作成でき、catchTagsでそれぞれのエラー型の場合の処理を書けます。
もちろん処理すると返り値のEffectのエラー型に反映されるので、各Effectがどんなエラーを返してくるかを正確に把握できます。

import {Console, Data, Effect} from "effect";

class NotFound extends Data.TaggedError("NotFound") {}
class Forbidden extends Data.TaggedError("Forbidden") {}
class IOException extends Data.TaggedError("IOException")<{message: string}> {}

// 発生しうるエラーはNotFound | Forbidden | IOException
const runApi: Effect.Effect<void, NotFound | Forbidden | IOException, never> = Effect.gen(function* () {
  yield* new Forbidden();  // TaggedErrorはそのままyield*で使える
  yield* new NotFound();
  yield* new IOException({message: "boom!"});
});


// NotFoundとForbiddenは処理したが、IOExceptionはまだ返ってくる可能性がある
const program: Effect.Effect<void, IOException, never> = runApi.pipe(
  Effect.catchTags({
    NotFound: () => Console.log("ないなった"),
    Forbidden: () => Console.log("めっ!"),
  }),
);

Effect.runPromise(program);
めっ!


他にも、すべてのエラーを把握する必要がある場合のvalidateなど便利なものが色々用意されています。

リトライ

リトライ機構って好みのものを作るのは大変ですが、Effectではリトライ機構やリトライ戦略を作るパーツが用意されています。
これだけでもEffectを使う価値ありそうですね!

Retrying – Effect Docs

その他

他にも

  • プリミティブ型にタグを付けて誤ったコンテキストで使われるのを防ぐ機能
  • 同時実行数の制御
  • RxやStream系ライブラリでよくあるようなスケジュール機能
    • 遅延
    • 繰り返し
  • テスト時に時刻を固定したり任意の時間を経過させられる機能
  • QueueやPubSub
  • テレメトリ
  • その他盛りだくさん

など、便利なものがたくさんあります!

Effect最&高

最後までお読みいただきありがとうございます。 この記事が皆さんの開発に少しでも役立つことを願っています。
Effectを使った最高の開発体験を楽しんでください🚀✨

そして、モナドの世界へようこそ。あなたが足を踏み入れたのは、コードの深淵なる世界。
非同期処理やエラー処理が錯綜する迷宮の中で、Effectがその道筋を示す光となります。その光を信じて進む先には、無限の可能性と確かな成功が待っています。
Effectの力と共に、未知の高みへと挑みましょう。

*1:ジェネレーター関数を使ってモナドの糖衣構文を実現してる

*2:ちなみにScalaのfor式と違ってyield*さえ書けば変数への代入は必須ではない