Я пытаюсь выяснить, как реализовать вспомогательный метод .flatMap для любой монады. В отличие от обычного .flatMap, он принимает лямбду, которая возвращает какое-то обычное значение вместо экземпляра Either. Он должен работать как адаптер для использования функций, которые не были предназначены для использования с Either.
В примере ниже я назвал этот метод адаптера .flatmap. Может кто-нибудь предложить, как я могу преобразовать эту функцию в допустимую перегрузку Typescript обычной .flatMap?
interface Either<TResult, TError> {
flatmap<R>(f: (value: TResult) => R): Either<R, void | TError>;
flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
}
class Left<TResult, TError extends { type: any }> implements Either<TResult, TError> {
public constructor(private readonly error: TError) {}
public flatmap<R>(f: (value: TResult) => R): Either<R, void | TError> {
return this.flatMap(n => new Left(this.error));
}
public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError> {
return new Left(this.error);
}
}
class Right<TResult, TError> implements Either<TResult, TError> {
public constructor(private readonly value: TResult) { }
public flatmap<R>(f: (value: TResult) => R): Either<R, void | TError> {
return new Right(f(this.value));
}
public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError> {
return f(this.value);
}
}
class ResourceError {
type = "ResourceError" as const
public constructor(public readonly resourceId: number) {}
}
class ObjectNotFound {
type = "ObjectNotFound" as const
public constructor(public readonly msg: string, public readonly timestamp: string) { }
}
class DivisionByZero {
type = "DivisionByZero" as const
}
function f1(s: string): Either<number, DivisionByZero> {
return new Right(+s);
}
function f2(n: number): Either<number, ResourceError> {
return new Right(n + 1);
}
function f3(n: number): Either<string, ObjectNotFound> {
return new Right(n.toString());
}
function f5(n: number): number {
return n * 10;
}
function f6(s: string): string {
return s + '!';
}
const c = f1('345')
.flatMap(n => f2(n))
.flatmap(n => f5(n))
.flatMap(n => f3(n))
.flatmap(n => f6(n));
console.log(c);
ОБНОВЛЕНИЕ: Для любопытного, более «инкапсулированного» API + статически проверенного соответствия шаблону:
type UnionOfNames<TUnion> = TUnion extends { type: any } ? TUnion["type"] : never
type UnionToMap<TUnion> = {
[NAME in UnionOfNames<TUnion>]: TUnion extends { type: NAME } ? TUnion : never
}
type Pattern<TResult, TMap> = {
[NAME in keyof TMap]: (value: TMap[NAME]) => TResult
}
type Matcher<TUnion, TResult> = Pattern<TResult, UnionToMap<TUnion>>;
interface Either<TResult, TError> {
flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;
match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R;
}
abstract class Either<TResult, TError> {
public static Right<TResult>(value: TResult): Either<TResult, never> {
return new Either.rightClass(
value,
x => x instanceof Either.leftClass || x instanceof Either.rightClass,
Either.Right
);
}
public static Left<TError extends { type: any }>(error: TError): Either<never, TError> {
return new Either.leftClass(error);
}
private static readonly leftClass =
class <TResult, TError extends { type: any }> implements Either<TResult, TError> {
public constructor(private readonly error: TError) { }
public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
public flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;
public flatMap(f: (value: TResult) => any) {
return this;
}
public match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R {
return (error as any)[this.error.type](this.error);
}
};
private static readonly rightClass =
class <TResult, TError> implements Either<TResult, TError> {
public constructor(
private readonly value: TResult,
private readonly isEitherInst: (x: any) => boolean,
private readonly rightFactory: <R>(result: R) => Either<R, TError>
) { }
public flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
public flatMap<R>(f: (value: TResult) => R): Either<R, void | TError>;
public flatMap(f: (value: TResult) => any) {
const result = f(this.value);
return this.isEitherInst(result) ? result : this.rightFactory(result);
}
public match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R {
return success(this.value);
}
}
}
class ResourceError {
type = "ResourceError" as const
public constructor(public readonly resourceId: number) { }
}
class ObjectNotFound {
type = "ObjectNotFound" as const
public constructor(public readonly msg: string, public readonly timestamp: string) { }
}
class DivisionByZero {
type = "DivisionByZero" as const
}
class NetworkError {
type = "NetworkError" as const
public constructor(public readonly address: string) {}
}
class GenericError {
type = "GenericError" as const
public constructor(public readonly exception: Error) {}
}
function f1(s: string): Either<number, DivisionByZero> {
console.log('f1()');
return Either.Right(+s);
}
function f2(n: number): Either<number, ResourceError> {
console.log('f2()');
return Either.Right(n + 1);
}
function f3(n: number): Either<string, ObjectNotFound> {
console.log('f3()');
return Either.Right(n.toString());
//return Either.Left(new ObjectNotFound('not found ', Date.now().toString()));
}
function f4(s: string): number {
console.log('f4()');
return +s * 10;
}
function f5(s: number): Either<string, ResourceError> {
console.log('f5()');
return Either.Right(s.toString() + '!');
}
const c = f1('345')
.flatMap(f2)
.flatMap(f3)
.flatMap(f4)
.flatMap(f5);
const r = c.match(
(result: any) => result.toString(),
{
//GenericError: (value: GenericError) => value.exception.message,
ObjectNotFound: (value: ObjectNotFound) => value.msg + value.timestamp,
ResourceError: (value: ResourceError) => 'resourceError',
DivisionByZero: (value: DivisionByZero) => 'divisionByZero',
}
);
console.log(r);
Ссылка на игровую площадку. Использование перегрузки - строки # 74 и # 76