Skip to content

Commit

Permalink
feat: Add more methods for Result (#267)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikuroXina authored Oct 30, 2024
1 parent b4df94d commit ce21d5b
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 1 deletion.
107 changes: 107 additions & 0 deletions src/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))),
Expand Down Expand Up @@ -136,6 +172,27 @@ Deno.test("andThen", () => {
);
});

Deno.test("asyncAndThen", async () => {
const sqrtThenToString = Result.asyncAndThen(
(num: number): Promise<Result.Result<string, string>> =>
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<number>(2);
const failure = Result.err<string>("not a 2");
Expand Down Expand Up @@ -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")),
Expand Down Expand Up @@ -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<string>();
// identity
Expand Down
121 changes: 120 additions & 1 deletion src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,42 @@ export const partialEqUnary = <E>(
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 =
<E>(catcher: (err: unknown) => E) =>
<A extends unknown[], R>(body: (...args: A) => R) =>
(...args: A): Result<E, R> => {
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 =
<E>(catcher: (err: unknown) => E) =>
<A extends unknown[], R>(body: (...args: A) => Promise<R>) =>
async (...args: A): Promise<Result<E, R>> => {
try {
return ok(await body(...args));
} catch (error: unknown) {
return err(catcher(error));
}
};

/**
* Maps the value in variant by two mappers.
*
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -413,6 +449,17 @@ export const andThen =
<T, U, E>(fn: (t: T) => Result<E, U>) =>
(resA: Result<E, T>): Result<E, U> => 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 =
<T, U, F>(fn: (value: T) => Promise<Result<F, U>>) =>
<E extends F>(res: Result<E, T>): Promise<Result<E | F, U>> =>
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.
*
Expand Down Expand Up @@ -590,6 +637,17 @@ export const mapErr =
<E, F>(fn: (t: E) => F) => <T>(res: Result<E, T>): Result<F, T> =>
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 =
<T, U>(mapper: (value: T) => Promise<U>) =>
async <E>(res: Result<E, T>): Promise<Result<E, U>> =>
isOk(res) ? ok(await mapper(res[1])) : res;

/**
* Makes a result of product from the two results.
*
Expand Down Expand Up @@ -668,6 +726,67 @@ export const resOptToOptRes = <E, T>(
return none();
};

/**
* A success return type of {@link Result.collect : `Result.collect`}.
*/
export type CollectSuccessValue<R extends readonly Result<unknown, unknown>[]> =
R extends readonly [] ? never[]
: {
-readonly [K in keyof R]: R[K] extends Result<unknown, infer T> ? T
: never;
};

/**
* An error return type of {@link Result.collect : `Result.collect`}.
*/
export type CollectErrorValue<R extends readonly Result<unknown, unknown>[]> =
R[number] extends Err<infer E> ? E
: R[number] extends Ok<unknown> ? never
: R[number] extends Result<infer E, unknown> ? E
: never;

/**
* A return type of {@link Result.collect : `Result.collect`}.
*/
export type CollectReturn<R extends readonly Result<unknown, unknown>[]> =
Result<CollectErrorValue<R>, CollectSuccessValue<R>>;

/**
* 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 = <const R extends readonly Result<unknown, unknown>[]>(
results: R,
): CollectReturn<R> => {
const successes: unknown[] = [];
for (const res of results) {
if (isErr(res)) {
return res as CollectReturn<R>;
}
successes.push(res[1]);
}
return ok(
successes,
) as CollectReturn<R>;
};

/**
* 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 =
<T>(inspector: (value: T) => void) =>
<E>(res: Result<E, T>): Result<E, T> => {
if (isOk(res)) {
inspector(res[1]);
}
return res;
};

/**
* Applies the function to another value on `Result`.
*
Expand Down

0 comments on commit ce21d5b

Please sign in to comment.