Add Option fold and methods/functions for Option/Either conversion

This commit is contained in:
Eric Rumsey 2025-06-13 11:18:15 -05:00
parent a23edf3605
commit b71d39c845
2 changed files with 175 additions and 4 deletions

View File

@ -1,3 +1,5 @@
import { Option } from './option'
/** /**
* Represents the failure case of an Either * Represents the failure case of an Either
* @typeParam E - The type of the error * @typeParam E - The type of the error
@ -77,6 +79,50 @@ export const fold =
(fa: Either<A, E>): B => (fa: Either<A, E>): B =>
fa._tag === 'Left' ? onLeft(fa.left) : onRight(fa.right) fa._tag === 'Left' ? onLeft(fa.left) : onRight(fa.right)
/**
* Converts an Option to an Either, providing a default error for None.
* @typeParam A - Value type
* @typeParam E - Error type
* @param onNone - Error value for None case
* @returns Function that converts Option<A> to Either<A, E>
*/
export const fromOption =
<E>(onNone: E) => <A>(option: Option<A>): Either<A, E> =>
option._tag === 'Some' ? right(option.value) : left(onNone)
/**
* Lazily converts an Option to an Either (error evaluated only when needed).
* @typeParam A - Value type
* @typeParam E - Error type
* @param onNone - Function returning error for None case
* @returns Function that converts Option<A> to Either<A, E>
*/
export const fromOptionL =
<E>(onNone: () => E) => <A>(option: Option<A>): Either<A, E> =>
option._tag === 'Some' ? right(option.value) : left(onNone())
/**
* Converts a Promise<Option> to Promise<Either> with a fixed error.
* @typeParam A - Value type
* @typeParam E - Error type
* @param onNone - Error value for None
* @returns Function that converts Promise<Option<A>> to Promise<Either<A, E>>
*/
export const fromPromiseOption =
<E>(onNone: E) => <A>(p: Promise<Option<A>>): Promise<Either<A, E>> =>
p.then(option => fromOption(onNone)(option))
/**
* Lazily converts a Promise<Option> to Promise<Either>.
* @typeParam A - Value type
* @typeParam E - Error type
* @param onNone - Function returning error for None
* @returns Function that converts Promise<Option<A>> to Promise<Either<A, E>>
*/
export const fromPromiseOptionL =
<E>(onNone: () => E) => <A>(p: Promise<Option<A>>): Promise<Either<A, E>> =>
p.then(option => fromOptionL(onNone)(option))
/** /**
* A class for working with Either in a do-notation style * A class for working with Either in a do-notation style
* @typeParam T - The type of the accumulated scope * @typeParam T - The type of the accumulated scope
@ -277,8 +323,7 @@ export class AsyncDoEither<T extends Record<string, unknown>, E> {
* @param f - Function returning Either or Promise of Either * @param f - Function returning Either or Promise of Either
* @returns A new AsyncDoEither instance with the same scope * @returns A new AsyncDoEither instance with the same scope
*/ */
doL(f: doL(f: (scope: T) =>
(scope: T) =>
| Either<unknown, E> | Either<unknown, E>
| Promise<Either<unknown, E>>): AsyncDoEither<T, E> | Promise<Either<unknown, E>>): AsyncDoEither<T, E>
{ {

View File

@ -1,3 +1,5 @@
import { Either, left, right } from './either'
/** /**
* Represents the absence of a value * Represents the absence of a value
*/ */
@ -52,6 +54,54 @@ export const chain =
<A, B>(f: (a: A) => Option<B>) => (fa: Option<A>): Option<B> => <A, B>(f: (a: A) => Option<B>) => (fa: Option<A>): Option<B> =>
fa._tag === 'Some' ? f(fa.value) : none fa._tag === 'Some' ? f(fa.value) : none
/**
* Folds an Option into a single value by providing handlers for both cases (Some/None)
* @typeParam A - The value type contained in the Option
* @typeParam B - The output type of the fold operation
* @param onNone - Function to handle the None case
* @param onSome - Function to handle the Some case
* @returns A function that takes an Option and returns the folded value
* @example
* const result = fold(
* () => 'No value',
* (value: number) => `Value: ${value}`
* )(some(42)) // Returns "Value: 42"
*
* @example
* const result = fold(
* () => 'No value',
* (value: number) => `Value: ${value}`
* )(none) // Returns "No value"
*/
export const fold =
<A, B>(onNone: () => B, onSome: (value: A) => B) => (option: Option<A>): B =>
option._tag === 'Some' ? onSome(option.value) : onNone()
/**
* A curried version of fold that takes the Option first
* @typeParam A - The value type contained in the Option
* @typeParam B - The output type of the fold operation
* @param option - The Option to fold
* @returns An object with methods to handle both cases
* @example
* some(42).fold({
* onNone: () => 'No value',
* onSome: (value) => `Value: ${value}`
* }) // Returns "Value: 42"
*/
export const foldC = <A>(option: Option<A>) => ({
/**
* @param handlers - Object containing both case handlers
* @param handlers.onNone - Function to handle None case
* @param handlers.onSome - Function to handle Some case
* @returns The folded value
*/
fold: <B>(handlers: {onNone: () => B, onSome: (value: A) => B}): B =>
option._tag === 'Some'
? handlers.onSome(option.value)
: handlers.onNone(),
})
/** /**
* Extracts the value from an Option or returns a default * Extracts the value from an Option or returns a default
* @typeParam A - The value type * @typeParam A - The value type
@ -155,6 +205,42 @@ export class DoOption<T extends Record<string, unknown>> {
return this.option return this.option
} }
/**
* Converts the DoOption to an Either, providing a default error value for the None case
* @typeParam E - The type of the error to use if the Option is None
* @param onNone - The error value to use if the Option is None
* @returns An Either - Right with the accumulated scope if the Option is Some,
* or Left with the provided error if the Option is None
* @example
* DoOption.start()
* .bind('id', some(1))
* .toEither('Missing value')
* // Returns Right({ id: 1 }) or Left('Missing value')
*/
toEither<E>(onNone: E): Either<T, E> {
return this.option._tag === 'Some'
? right(this.option.value)
: left(onNone)
}
/**
* Converts the DoOption to an Either, lazily providing an error value for the None case
* @typeParam E - The type of the error to use if the Option is None
* @param onNone - A function that returns the error value when Option is None
* @returns An Either - Right with the accumulated scope if the Option is Some,
* or Left with the computed error if the Option is None
* @example
* DoOption.start()
* .bind('config', loadConfig())
* .toEitherL(() => new Error('Config missing'))
* // Returns Right(config) or Left(Error)
*/
toEitherL<E>(onNone: () => E): Either<T, E> {
return this.option._tag === 'Some'
? right(this.option.value)
: left(onNone())
}
/** /**
* Executes an effect without modifying the scope * Executes an effect without modifying the scope
* @param fa - The Option effect to execute (does not depend on scope) * @param fa - The Option effect to execute (does not depend on scope)
@ -303,6 +389,46 @@ export class AsyncDoOption<T extends Record<string, unknown>> {
) )
} }
/**
* Converts the AsyncDoOption to a Promise of Either, providing a default error value for the None case
* @typeParam E - The type of the error to use if the Option is None
* @param onNone - The error value to use if the Option is None
* @returns A Promise that resolves to an Either - Right with the accumulated scope if the Option is Some,
* or Left with the provided error if the Option is None
* @example
* AsyncDoOption.start()
* .bind('user', getUserAsync())
* .toEither('User not found')
* .then(either => { ... }) // either is Either<{ user: User }, string>
*/
toEither<E>(onNone: E): Promise<Either<T, E>> {
return this.promise.then(option =>
option._tag === 'Some'
? right(option.value)
: left(onNone)
)
}
/**
* Converts the AsyncDoOption to a Promise of Either, lazily providing an error value for the None case
* @typeParam E - The type of the error to use if the Option is None
* @param onNone - A function that returns the error value to use if the Option is None
* @returns A Promise that resolves to an Either - Right with the accumulated scope if the Option is Some,
* or Left with the error from onNone if the Option is None
* @example
* AsyncDoOption.start()
* .bind('data', fetchData())
* .toEitherL(() => new Error('Data fetch failed'))
* .then(either => { ... }) // either is Either<{ data: Data }, Error>
*/
toEitherL<E>(onNone: () => E): Promise<Either<T, E>> {
return this.promise.then(option =>
option._tag === 'Some'
? right(option.value)
: left(onNone())
)
}
/** /**
* Returns the current scope * Returns the current scope
* @returns A promise of the current Option scope * @returns A promise of the current Option scope