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
側の generator
に yield*
で処理を委譲しているという点にあり、
これにより 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
を剥がす操作で Result
が Failure
なら、ついでに Return
してしまえます
これは便利だなと思いました
Cons
Result の型を事前に決める必要がある
今回は gen
で NoInfer
で明示的に型の推論をしないようにしています
これは Generator
が yield
や next
が毎回同じ型であることを期待するためです
このため、事前に共通の型の情報を与えなければならず、
実際に起こりえるエラー型の直和型をコードから直接推論することができません...
ここら辺をうまくできる方法があったら知りたいです...
まとめ
Effect.gen
がどうやって個々の型を推論しているのか謎でしたが
ステップバイステップで追っていくと、中心的な理屈がわかりました
ここで紹介した例は Generator
でしたが、AsyncGenerator
とすれば
非同期処理もまとめて扱えてお得になります
基本的には AsyncGenerator
をラップすることになると思います
でも yield*
を使う理由が型推論だけという理由なので、
あまりプロダクションコードに入れる気は (まだ) していないです...