我试图弄清楚如何在任一monad上实现辅助.flatMap方法。与常规.flatMap不同,它接受一个lambda,该lambda返回一些常规值而不是Either实例。它应该像适配器一样工作,以使用不是为Either设计的功能。
在下面的示例中,我将此适配器方法命名为.flatmap。谁能建议我如何将该函数转换为常规.flatMap的合法Typescript重载?
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);
Link到操场。过载的用法-第74和#76行
答案 0 :(得分:1)
overload呼叫签名应按照从最严格到最不严格的顺序排列,所以我可能会这样:
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>;
}
由于类型f
的任何(value: TResult) => Either<R, E>
也会 成为类型(value: TResult) => R
的值(对于不同的R
),但是相反是不正确的,我们必须这样做。如果我们取消订单,则不会将任何呼叫视为=> Either<R, E>
呼叫签名,因为它始终与更通用的=> R
签名匹配。
实现重载需要单个实现签名(因为在运行时只有一个功能),因此您需要编写运行时代码来弄清楚在处理不同类型的f
时应执行的操作。
您的Left
实现似乎完全独立于f
,因此这很容易实现:
class Left<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 new Left(this.error);
}
}
对于Right
实现,我们需要确定f(this.value)
在运行时是否构成Either
。假设只有Left
和Right
类实现Either
,我可以这样:
class Right<TResult, TError> implements Either<TResult, TError> {
public constructor(private readonly value: TResult) { }
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 ret = f(this.value);
return ret instanceof Left || ret instanceof Right ? ret : new Right(ret);
}
}
这在您的示例代码上似乎可以正常运行:
const c = f1('345').flatMap(f2).flatMap(f5).flatMap(f3).flatMap(f6);
console.log(c); // Object { value: "3460!" }
好的,希望能有所帮助;祝你好运!