0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Effect.gen みたいな関数を作ってみよう

Last updated at Posted at 2024-07-15

Result 型を便利に使いたい!

最近 Domain Model Made Functional を読みました

これを TypeScript で表現してみたいけど、Railway Oriented はやりすぎじゃ...
って思った矢先に、Effect.gen で Rust みたく Result を使えるっぽい!?

純関数型的に表現するのを、ちょっと妥協したいなと思っていたので
Railway Oriented じゃない Result をどうやるのか見てみたいと思いました

そもそも Effect.gen って魔法みたいじゃないですか?

そもそも、型定義を見てもよくわからない...

僕が気になったのは以下の点です

  • なぜ yeild ではなくて yield* なのか?
  • なぜ Generator なのに個々の yield* に対して型を推論できるのか?

では、実際に Effect.gen っぽい関数群を作ってみて、なぜなのか確かめてみましょう

実際に作ってみよう!

基本的な Result の定義

error: boolean で場合分けする Result の定義は、こんな感じになります

type Success<T,> = T & { error: false };
type Failure<F,> = F & { error: true };
type Result<T, F> = Success<T> | Failure<F>;

const toSuccess = <T,>(success: T) => ({ ... success, error: false }) as Result<T, never>;
const toFailure = <F,>(failure: F) => ({ ... failure, error: true }) as Result<never, F>;
const extractSuccess = <T,>(success: Success<T>) => { const { error, ... rest } = success; return rest; };
const extractFailure = <F,>(failure: Failure<F>) => { const { error, ... rest } = failure; return rest; };

gen の定義

上記の Result を利用する gen の定義は、このような感じになります
ここの型はそんなに重要ではないので unknown で逃げています

function gen<T, F>(definision: () => Generator<Result<unknown, NoInfer<F>>, Success<NoInfer<T>>, unknown>) {
  const generator = definision();
  let result = generator.next();
  if (result.done) { return result.value; }
  if (result.value.error) { return result.value; }

  while (true) {
    result = generator.next(extractSuccess(result.value));
    if (result.done) { return result.value; }
    if (result.value.error) { return result.value; }
  }
}
// definition: () => Generator<Result<unknown, NoInfer<F>>, Success<T>, unknown>) として Success<T> は推論してもいいです

ユーティリティ関数の定義

ユーティリティとして Result<T, F> を受け取って T を受け取り
T をそのまま return する Generator を定義します

この関数がかなりのキモなポイントです

function* unwrap<T, F>(a: Result<T, F>): Generator<Result<T, F>, T, T> {
  return yield a;
}

実際の利用例

使う側はこうやって Effect.gen と (ある程度) 同じメンタリティで使えます

キモは unrwap 側の generatoryield* で処理を委譲しているという点にあり、
これにより yeild* 毎に unwrap の結果を参照するため、型がその都度決まります

const result = gen((function* () {
  const a = yield* unwrap(toSuccess({ hello: 'world', num: 5 } as const));
  let z = a.num < Math.random() * 10 ? toSuccess({ y: 'test' }) : toFailure({ x: 'fail' });
  const b = yield* unwrap(z);
  return toSuccess(b);
});

// この result をよしなにと...

Pros/Cons

Pros

命令的に Result の Implicit な Early Return が表現できる

元々やりたいと思っていた、命令的な Early Return ができます
Result を剥がす操作で ResultFailure なら、ついでに Return してしまえます

これは便利だなと思いました

Cons

Result の型を事前に決める必要がある

今回は genNoInfer で明示的に型の推論をしないようにしています
これは Generatoryieldnext が毎回同じ型であることを期待するためです

このため、事前に共通の型の情報を与えなければならず、
実際に起こりえるエラー型の直和型をコードから直接推論することができません...

ここら辺をうまくできる方法があったら知りたいです...

まとめ

Effect.gen がどうやって個々の型を推論しているのか謎でしたが
ステップバイステップで追っていくと、中心的な理屈がわかりました

ここで紹介した例は Generator でしたが、AsyncGenerator とすれば
非同期処理もまとめて扱えてお得になります
基本的には AsyncGenerator をラップすることになると思います

でも yield* を使う理由が型推論だけという理由なので、
あまりプロダクションコードに入れる気は (まだ) していないです...

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?