From ce21d5b1e6dc4bbd1369952b745c7f89af1dd790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikuro=E3=81=95=E3=81=84=E3=81=AA?= <10331164+MikuroXina@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:05:50 +0900 Subject: [PATCH] feat: Add more methods for Result (#267) --- src/result.test.ts | 107 +++++++++++++++++++++++++++++++++++++++ src/result.ts | 121 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 227 insertions(+), 1 deletion(-) diff --git a/src/result.test.ts b/src/result.test.ts index 658bc4e1..ec7fbb79 100644 --- a/src/result.test.ts +++ b/src/result.test.ts @@ -73,6 +73,42 @@ Deno.test("total order", () => { ); }); +Deno.test("wrapThrowable", () => { + const safeSqrt = Result.wrapThrowable((err) => err as Error)( + (x: number) => { + if (!(x >= 0)) { + throw new RangeError("x must be positive or a zero"); + } + return Math.sqrt(x); + }, + ); + assertEquals(safeSqrt(4), Result.ok(2)); + assertEquals(safeSqrt(0), Result.ok(0)); + assertEquals( + safeSqrt(-1), + Result.err(new RangeError("x must be positive or a zero")), + ); +}); + +Deno.test("wrapAsyncThrowable", async () => { + const safeSqrt = Result.wrapAsyncThrowable((err) => err as Error)( + (x: number) => { + if (!(x >= 0)) { + return Promise.reject( + new RangeError("x must be positive or a zero"), + ); + } + return Promise.resolve(Math.sqrt(x)); + }, + ); + assertEquals(await safeSqrt(4), Result.ok(2)); + assertEquals(await safeSqrt(0), Result.ok(0)); + assertEquals( + await safeSqrt(-1), + Result.err(new RangeError("x must be positive or a zero")), + ); +}); + Deno.test("flatten", () => { assertEquals( Result.flatten(Result.ok(Result.ok("hello"))), @@ -136,6 +172,27 @@ Deno.test("andThen", () => { ); }); +Deno.test("asyncAndThen", async () => { + const sqrtThenToString = Result.asyncAndThen( + (num: number): Promise> => + Promise.resolve( + num < 0 + ? Result.err("num must not be negative") + : Result.ok(Math.sqrt(num).toString()), + ), + ); + + assertEquals(await sqrtThenToString(Result.ok(4)), Result.ok("2")); + assertEquals( + await sqrtThenToString(Result.ok(-1)), + Result.err("num must not be negative"), + ); + assertEquals( + await sqrtThenToString(Result.err("not a number")), + Result.err("not a number"), + ); +}); + Deno.test("or", () => { const success = Result.ok(2); const failure = Result.err("not a 2"); @@ -204,6 +261,15 @@ Deno.test("mapErr", () => { assertEquals(prefix(Result.err("failure")), Result.err("LOG: failure")); }); +Deno.test("asyncMap", async () => { + const len = Result.asyncMap((text: string) => Promise.resolve(text.length)); + + assertEquals(await len(Result.ok("")), Result.ok(0)); + assertEquals(await len(Result.ok("foo")), Result.ok(3)); + assertEquals(await len(Result.err("fail")), Result.err("fail")); + assertEquals(await len(Result.err(-1)), Result.err(-1)); +}); + Deno.test("product", () => { assertEquals( Result.product(Result.ok("foo"))(Result.ok("bar")), @@ -261,6 +327,47 @@ Deno.test("transpose", () => { ); }); +Deno.test("collect", () => { + assertEquals(Result.collect([]), Result.ok([])); + + assertEquals( + Result.collect([ + Result.ok(3), + Result.ok("1"), + ]), + Result.ok([3, "1"] as [number, string]), + ); + + assertEquals( + Result.collect([ + Result.ok(3), + Result.err("1"), + Result.ok(4n), + Result.err(new Error("wow")), + ]), + Result.err("1"), + ); + assertEquals( + Result.collect([ + Result.ok(3), + Result.err(new Error("wow")), + Result.ok(4n), + Result.err("1"), + ]), + Result.err(new Error("wow")), + ); +}); + +Deno.test("inspect", () => { + Result.inspect((value: string) => { + assertEquals(value, "foo"); + })(Result.ok("foo")); + + Result.inspect(() => { + throw new Error("unreachable"); + })(Result.err(42)); +}); + Deno.test("functor laws", () => { const f = Result.functor(); // identity diff --git a/src/result.ts b/src/result.ts index bb10791b..f5ce27d6 100644 --- a/src/result.ts +++ b/src/result.ts @@ -258,6 +258,42 @@ export const partialEqUnary = ( partialEquality({ equalityE: equalityE, equalityT: { eq: equality } }), }); +/** + * Wraps the return value of `body` into a {@link Result.Result | `Result`}. + * + * @param catcher - The function to cast an error from `body`. + * @param body - The function to be wrapped. + * @returns The wrapped function. + */ +export const wrapThrowable = + (catcher: (err: unknown) => E) => + (body: (...args: A) => R) => + (...args: A): Result => { + try { + return ok(body(...args)); + } catch (error: unknown) { + return err(catcher(error)); + } + }; + +/** + * Wraps the return value of `body` into a {@link Result.Result | `Result`} over `Promise`. + * + * @param catcher - The function to cast an error from `body`. + * @param body - The asynchronous function to be wrapped. + * @returns The wrapped function. + */ +export const wrapAsyncThrowable = + (catcher: (err: unknown) => E) => + (body: (...args: A) => Promise) => + async (...args: A): Promise> => { + try { + return ok(await body(...args)); + } catch (error: unknown) { + return err(catcher(error)); + } + }; + /** * Maps the value in variant by two mappers. * @@ -385,7 +421,7 @@ export const and = isOk(resA) ? resB : resA; /** - * Returns `fn()` if `resA` is an `Ok`, otherwise returns the error `resA`. This is an implementation of `FlatMap`. The order of arguments is reversed because of that it is useful for partial applying. + * Returns `fn(v)` if `resA` is an `Ok(v)`, otherwise returns the error `resA`. This is an implementation of `FlatMap`. The order of arguments is reversed because of that it is useful for partial applying. * * @param fn - The function provides a second result. * @param resA - The first result. @@ -413,6 +449,17 @@ export const andThen = (fn: (t: T) => Result) => (resA: Result): Result => isOk(resA) ? fn(resA[1]) : resA; +/** + * Returns `fn(v)` if `res` is an `Ok(v)`, otherwise the error `res`. The order of arguments is reversed because of that it is useful for partial applying. + * + * @param fn - The function which provides a second result. + * @returns + */ +export const asyncAndThen = + (fn: (value: T) => Promise>) => + (res: Result): Promise> => + isOk(res) ? fn(res[1]) : Promise.resolve(res); + /** * Returns `resB` if `resA` is an `Err`, otherwise returns the success `resA`. The order of arguments is reversed because of that it is useful for partial applying. * @@ -590,6 +637,17 @@ export const mapErr = (fn: (t: E) => F) => (res: Result): Result => isErr(res) ? err(fn(res[1])) : res; +/** + * Maps a {@link Result.Result | `Result`} with `mapper` over `Promise`. + * + * @param mapper - The asynchronous function from `T` to `U`. + * @returns The mapped one on `Promise`. + */ +export const asyncMap = + (mapper: (value: T) => Promise) => + async (res: Result): Promise> => + isOk(res) ? ok(await mapper(res[1])) : res; + /** * Makes a result of product from the two results. * @@ -668,6 +726,67 @@ export const resOptToOptRes = ( return none(); }; +/** + * A success return type of {@link Result.collect : `Result.collect`}. + */ +export type CollectSuccessValue[]> = + R extends readonly [] ? never[] + : { + -readonly [K in keyof R]: R[K] extends Result ? T + : never; + }; + +/** + * An error return type of {@link Result.collect : `Result.collect`}. + */ +export type CollectErrorValue[]> = + R[number] extends Err ? E + : R[number] extends Ok ? never + : R[number] extends Result ? E + : never; + +/** + * A return type of {@link Result.collect : `Result.collect`}. + */ +export type CollectReturn[]> = + Result, CollectSuccessValue>; + +/** + * Transforms the list of {@link Result.Result | `Results`}s into success values or the first error. + * + * @param results - The list of {@link Result.Result | `Results`}s. + * @returns Success values or the first error. + */ +export const collect = []>( + results: R, +): CollectReturn => { + const successes: unknown[] = []; + for (const res of results) { + if (isErr(res)) { + return res as CollectReturn; + } + successes.push(res[1]); + } + return ok( + successes, + ) as CollectReturn; +}; + +/** + * Inspects the success value of a {@link Result.Result | `Result`} and returns as is. + * + * @param inspector - The function to get a success value. + * @returns The original one. + */ +export const inspect = + (inspector: (value: T) => void) => + (res: Result): Result => { + if (isOk(res)) { + inspector(res[1]); + } + return res; + }; + /** * Applies the function to another value on `Result`. *