313 lines
9.1 KiB
TypeScript
313 lines
9.1 KiB
TypeScript
/**
|
|
* Represents the failure case of an Either
|
|
* @typeParam E - The type of the error
|
|
*/
|
|
export type Left<E> = {readonly _tag: 'Left', readonly left: E}
|
|
|
|
/**
|
|
* Represents the success case of an Either
|
|
* @typeParam A - The type of the value
|
|
*/
|
|
export type Right<A> = {readonly _tag: 'Right', readonly right: A}
|
|
|
|
/**
|
|
* A type that represents either a success (Right) or failure (Left)
|
|
* @typeParam A - The success value type
|
|
* @typeParam E - The error type
|
|
*/
|
|
export type Either<A, E> = Right<A> | Left<E>
|
|
|
|
/**
|
|
* Constructs a Left value
|
|
* @typeParam E - The error type
|
|
* @param e - The error value
|
|
* @returns A Left containing the error
|
|
*/
|
|
export const left = <E>(e: E): Either<never, E> => ({
|
|
_tag: 'Left',
|
|
left: e,
|
|
})
|
|
|
|
/**
|
|
* Constructs a Right value
|
|
* @typeParam A - The value type
|
|
* @param a - The value
|
|
* @returns A Right containing the value
|
|
*/
|
|
export const right = <A>(a: A): Either<A, never> => ({
|
|
_tag: 'Right',
|
|
right: a,
|
|
})
|
|
|
|
/**
|
|
* Maps a function over the Right side of an Either
|
|
* @typeParam A - The input value type
|
|
* @typeParam B - The output value type
|
|
* @typeParam E - The error type
|
|
* @param f - The mapping function
|
|
* @returns A function that takes an Either and returns a new Either
|
|
*/
|
|
export const eitherMap =
|
|
<A, B, E>(f: (a: A) => B) => (fa: Either<A, E>): Either<B, E> =>
|
|
fa._tag === 'Right' ? right(f(fa.right)) : fa
|
|
|
|
/**
|
|
* Chains computations that may return Either values
|
|
* @typeParam A - The input value type
|
|
* @typeParam B - The output value type
|
|
* @typeParam E - The error type
|
|
* @param f - The function that returns an Either
|
|
* @returns A function that takes an Either and returns a new Either
|
|
*/
|
|
export const eitherChain =
|
|
<A, B, E>(f: (a: A) => Either<B, E>) => (fa: Either<A, E>): Either<B, E> =>
|
|
fa._tag === 'Right' ? f(fa.right) : fa
|
|
|
|
/**
|
|
* Folds an Either into a single value by providing handlers for both cases
|
|
* @typeParam A - The success value type
|
|
* @typeParam E - The error type
|
|
* @typeParam B - The output type
|
|
* @param onLeft - Function to handle Left case
|
|
* @param onRight - Function to handle Right case
|
|
* @returns A function that takes an Either and returns the folded value
|
|
*/
|
|
export const fold =
|
|
<A, E, B>(onLeft: (e: E) => B, onRight: (a: A) => B) =>
|
|
(fa: Either<A, E>): B =>
|
|
fa._tag === 'Left' ? onLeft(fa.left) : onRight(fa.right)
|
|
|
|
/**
|
|
* A class for working with Either in a do-notation style
|
|
* @typeParam T - The type of the accumulated scope
|
|
* @typeParam E - The error type
|
|
*/
|
|
export class DoEither<T extends Record<string, unknown>, E> {
|
|
/**
|
|
* @param either - The current Either value
|
|
*/
|
|
constructor(private readonly either: Either<T, E>) {}
|
|
|
|
/**
|
|
* Binds a new value to the scope
|
|
* @typeParam K - The key to bind to
|
|
* @typeParam A - The type of the value to bind
|
|
* @param key - The property name to bind to
|
|
* @param fa - The Either value to bind
|
|
* @returns A new DoEither instance with the extended scope
|
|
*/
|
|
bind<K extends string, A>(
|
|
key: K,
|
|
fa: Either<A, E>,
|
|
): DoEither<T & Record<K, A>, E> {
|
|
const newEither = eitherChain<T, T & Record<K, A>, E>((scope: T) =>
|
|
eitherMap<A, T & Record<K, A>, E>((a: A) => ({...scope, [key]: a}))(fa)
|
|
)(this.either)
|
|
return new DoEither(newEither)
|
|
}
|
|
|
|
/**
|
|
* Binds a new value to the scope using a lazily evaluated Either
|
|
* @typeParam K - The key to bind to
|
|
* @typeParam A - The type of the value to bind
|
|
* @param key - The property name to bind to
|
|
* @param f - Function returning the Either value to bind (receives current scope)
|
|
* @returns A new DoEither instance with the extended scope
|
|
*/
|
|
bindL<K extends string, A>(
|
|
key: K,
|
|
f: (scope: T) => Either<A, E>,
|
|
): DoEither<T & Record<K, A>, E> {
|
|
const newEither = eitherChain<T, T & Record<K, A>, E>((scope: T) =>
|
|
eitherMap<A, T & Record<K, A>, E>((a: A) => ({...scope, [key]: a}))(
|
|
f(scope),
|
|
)
|
|
)(this.either)
|
|
return new DoEither(newEither)
|
|
}
|
|
|
|
/**
|
|
* Returns the final result from the scope
|
|
* @typeParam B - The return type
|
|
* @param f - The function to transform the scope
|
|
* @returns An Either containing the result
|
|
*/
|
|
return<B>(f: (scope: T) => B): Either<B, E> {
|
|
return eitherMap<T, B, E>(f)(this.either)
|
|
}
|
|
|
|
/**
|
|
* Returns the current scope
|
|
* @returns The current Either scope
|
|
*/
|
|
done(): Either<T, E> {
|
|
return this.either
|
|
}
|
|
|
|
/**
|
|
* Executes an effect without modifying the scope
|
|
* @param fa - The Either effect to execute
|
|
* @returns A new DoEither instance with the same scope
|
|
*/
|
|
do(fa: Either<unknown, E>): DoEither<T, E> {
|
|
const newEither = eitherChain<T, T, E>((scope: T) =>
|
|
eitherMap<unknown, T, E>(() => scope)(fa)
|
|
)(this.either)
|
|
return new DoEither(newEither)
|
|
}
|
|
|
|
/**
|
|
* Executes an effect that depends on the current scope
|
|
* @param f - The function that returns an Either effect
|
|
* @returns A new DoEither instance with the same scope
|
|
*/
|
|
doL(f: (scope: T) => Either<unknown, E>): DoEither<T, E> {
|
|
const newEither = eitherChain<T, T, E>((scope: T) =>
|
|
eitherMap<unknown, T, E>(() => scope)(f(scope))
|
|
)(this.either)
|
|
return new DoEither(newEither)
|
|
}
|
|
|
|
/**
|
|
* Starts a new DoEither chain with an empty scope
|
|
* @returns A new DoEither instance with an empty scope
|
|
*/
|
|
static start<E>(): DoEither<{}, E> {
|
|
return new DoEither(right({}))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A class for working with Either in a do-notation style with asynchronous computations
|
|
* @typeParam T - The type of the accumulated scope
|
|
* @typeParam E - The error type
|
|
*/
|
|
export class AsyncDoEither<T extends Record<string, unknown>, E> {
|
|
/**
|
|
* @param promise - The current Promise of an Either value
|
|
*/
|
|
constructor(private readonly promise: Promise<Either<T, E>>) {}
|
|
|
|
/**
|
|
* Binds a new value to the scope synchronously
|
|
* @typeParam K - The key to bind to
|
|
* @typeParam A - The type of the value to bind
|
|
* @param key - The property name to bind to
|
|
* @param fa - The Either value to bind
|
|
* @returns A new AsyncDoEither instance with the extended scope
|
|
*/
|
|
bind<K extends string, A>(
|
|
key: K,
|
|
fa: Either<A, E>,
|
|
): AsyncDoEither<T & Record<K, A>, E> {
|
|
return this.bindL(key, () => fa)
|
|
}
|
|
|
|
/**
|
|
* Binds a new value to the scope asynchronously
|
|
* @typeParam K - The key to bind to
|
|
* @typeParam A - The type of the value to bind
|
|
* @param key - The property name to bind to
|
|
* @param f - Function returning Either or Promise of Either
|
|
* @returns A new AsyncDoEither instance with the extended scope
|
|
*/
|
|
bindL<K extends string, A>(
|
|
key: K,
|
|
f: (scope: T) => Either<A, E> | Promise<Either<A, E>>,
|
|
): AsyncDoEither<T & Record<K, A>, E> {
|
|
const newPromise = this.promise.then(async (currentEither) => {
|
|
if (isLeft(currentEither)) {
|
|
return currentEither as unknown as Either<T & Record<K, A>, E>
|
|
}
|
|
|
|
const scope = currentEither.right
|
|
try {
|
|
const nextEither = await f(scope)
|
|
|
|
if (isLeft(nextEither)) {
|
|
return nextEither as unknown as Either<T & Record<K, A>, E>
|
|
}
|
|
|
|
return right({
|
|
...scope,
|
|
[key]: nextEither.right,
|
|
}) as Either<T & Record<K, A>, E>
|
|
} catch (err) {
|
|
return left(err as E) as Either<T & Record<K, A>, E>
|
|
}
|
|
})
|
|
|
|
return new AsyncDoEither(newPromise)
|
|
}
|
|
|
|
/**
|
|
* Returns the final result from the scope
|
|
* @typeParam B - The return type
|
|
* @param f - The function to transform the scope
|
|
* @returns A Promise of Either containing the result
|
|
*/
|
|
return<B>(f: (scope: T) => B): Promise<Either<B, E>> {
|
|
return this.promise.then(either => {
|
|
if (isLeft(either)) {
|
|
return either as unknown as Either<B, E>
|
|
}
|
|
return right(f(either.right))
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Returns the current scope
|
|
* @returns A Promise of the current Either scope
|
|
*/
|
|
done(): Promise<Either<T, E>> {
|
|
return this.promise
|
|
}
|
|
|
|
/**
|
|
* Executes an effect without modifying the scope
|
|
* @param fa - The Either effect to execute
|
|
* @returns A new AsyncDoEither instance with the same scope
|
|
*/
|
|
do(fa: Either<unknown, E>): AsyncDoEither<T, E> {
|
|
return this.doL(() => fa)
|
|
}
|
|
|
|
/**
|
|
* Executes an effect that depends on the current scope
|
|
* @param f - Function returning Either or Promise of Either
|
|
* @returns A new AsyncDoEither instance with the same scope
|
|
*/
|
|
doL(f:
|
|
(scope: T) =>
|
|
| Either<unknown, E>
|
|
| Promise<Either<unknown, E>>): AsyncDoEither<T, E>
|
|
{
|
|
const newPromise = this.promise.then(async (currentEither) => {
|
|
if (isLeft(currentEither)) {
|
|
return currentEither
|
|
}
|
|
|
|
const scope = currentEither.right
|
|
try {
|
|
const effectEither = await f(scope)
|
|
if (isLeft(effectEither)) {
|
|
return effectEither as unknown as Either<T, E>
|
|
}
|
|
return currentEither
|
|
} catch (err) {
|
|
return left(err as E)
|
|
}
|
|
})
|
|
|
|
return new AsyncDoEither(newPromise)
|
|
}
|
|
|
|
/**
|
|
* Starts a new AsyncDoEither chain with an empty scope
|
|
* @returns A new AsyncDoEither instance with an empty scope
|
|
*/
|
|
static start<E>(): AsyncDoEither<{}, E> {
|
|
return new AsyncDoEither(Promise.resolve(right({})))
|
|
}
|
|
}
|