TypeScript 中的 monad + 模式匹配
Either monad in TypeScript + pattern matching
我正在尝试在 TypeScript 中实现 Either monad。
interface Either<TResult, TError> {
flatMap<R>(f: (value: TResult) => Either<R, TError>): Either<R, TError>;
match<R>(result: (success: TResult) => R, error: (err: TError) => R): R;
}
class Left<TResult, TError> implements Either<TResult, TError> {
public constructor(private readonly error: TError) {}
public flatMap<R>(f: (value: TResult) => Either<R, TError>): Either<R, TError> {
return new Left(this.error);
}
public match<R>(success: (value: TResult) => R, error: (err: TError) => R): R {
return error(this.error);
}
}
class Right<TResult, TError> implements Either<TResult, TError> {
public constructor(private readonly value: TResult) {}
public flatMap<R>(f: (value: TResult) => Either<R, TError>): Either<R, TError> {
return f(this.value);
}
public match<R>(success: (result: TResult) => R, error: (err: TError) => R): R {
return success(this.value);
}
}
class ResourceError {
}
class ObjectNotFound {
public constructor(public readonly msg: string, public readonly code: number) {
}
}
function f1(s: string): Either<number, ObjectNotFound> {
return new Right(+s);
}
function f2(n: number): Either<number, ResourceError> {
return new Right(n + 1);
}
function f3(n: string): Either<string, ObjectNotFound> {
return new Right(n.toString());
}
const c = f1('345')
// (*) line 58 - error: Type 'Either<number, ResourceError>' is not assignable to type 'Either<number, ObjectNotFound>'.
.flatMap(n => f2(n))
.flatMap(n => f3(n));
const r = c.match(
(result: string) => result,
(err: ObjectNotFound) => err.msg
);
Link 到 playground.
由于不同的函数可能会产生不同类型的错误,因此 flatMap 链接中断。
我假设,从代码中可以清楚地看出总体意图。我希望我选择了正确的工具(monad)。
任何人都可以提出修复建议以使其全部正常工作吗?
[更新]: 感谢 SpencerPark 向正确的方向推动。
.match 按最初预期工作的完整实现如下(受 this post 启发):
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>;
match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R;
}
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> {
return new Left(this.error);
}
public match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R {
return (error as any)[this.error.type](this.error);
}
}
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> {
return f(this.value);
}
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) {}
}
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 f4(s: string): Either<number, NetworkError> {
return new Left(new NetworkError('someAdress'));
}
const c = f1('345')
.flatMap(n => f2(n))
.flatMap(n => f3(n))
.flatMap(s => f4(s));
const r = c.match(
(result: number) => result.toString(),
{
ObjectNotFound: (value: ObjectNotFound) => 'objectNotFound',
ResourceError: (value: ResourceError) => 'resourceError',
DivisionByZero: (value: DivisionByZero) => 'divisionByZero',
NetworkError: (value: NetworkError) => value.address
}
);
console.log(r);
更新后的游乐场是 here。
[UDPDATE 2]:
'catchall' 子句支持:
type DiscriminatedUnion<T> = { type: T }
type UnionOfNames<TUnion> = TUnion extends DiscriminatedUnion<string> ? TUnion["type"] : never
type UnionToMap<TUnion> = {
[NAME in UnionOfNames<TUnion>]: TUnion extends DiscriminatedUnion<NAME> ? TUnion : never
}
type Pattern<TResult, TMap> = {
[NAME in keyof TMap]: (value: TMap[NAME]) => TResult
} | ({
[NAME in keyof TMap]?: (value: TMap[NAME]) => TResult
} & { catchall: (value: DiscriminatedUnion<string>) => TResult})
// ... SKIPPED ...
const r1 = c.match(
(result: string) => result,
{
// when there is the 'catchall' case, others are optional. But still only the possible cases can be listed.
ObjectNotFound: (value: ObjectNotFound) => value.msg + value.timestamp,
catchall: (value: DiscriminatedElement<string>) => 'catchall ' + value.type
}
);
我们可以将flatMap
的签名改成如下:
flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
这里的错误类型要么是前一次计算的错误,要么是return当前计算的错误。从概念上讲,这遵循 Either
模型。在您的示例中,链末尾的类型是预期的 Either<string, ObjectNotFound | ResourceError>
。 (修正看起来像是打字错误的内容后,f3(n: number)...
)。
整体计算可能 return 一个 ObjectNotFound
错误或 ResourceError
取决于失败的步骤。
最终匹配现在可以正确引发类型错误,因为 error
函数不处理 ResourceError
尽可能从 f2
.[=21= 引发的情况]
我正在尝试在 TypeScript 中实现 Either monad。
interface Either<TResult, TError> {
flatMap<R>(f: (value: TResult) => Either<R, TError>): Either<R, TError>;
match<R>(result: (success: TResult) => R, error: (err: TError) => R): R;
}
class Left<TResult, TError> implements Either<TResult, TError> {
public constructor(private readonly error: TError) {}
public flatMap<R>(f: (value: TResult) => Either<R, TError>): Either<R, TError> {
return new Left(this.error);
}
public match<R>(success: (value: TResult) => R, error: (err: TError) => R): R {
return error(this.error);
}
}
class Right<TResult, TError> implements Either<TResult, TError> {
public constructor(private readonly value: TResult) {}
public flatMap<R>(f: (value: TResult) => Either<R, TError>): Either<R, TError> {
return f(this.value);
}
public match<R>(success: (result: TResult) => R, error: (err: TError) => R): R {
return success(this.value);
}
}
class ResourceError {
}
class ObjectNotFound {
public constructor(public readonly msg: string, public readonly code: number) {
}
}
function f1(s: string): Either<number, ObjectNotFound> {
return new Right(+s);
}
function f2(n: number): Either<number, ResourceError> {
return new Right(n + 1);
}
function f3(n: string): Either<string, ObjectNotFound> {
return new Right(n.toString());
}
const c = f1('345')
// (*) line 58 - error: Type 'Either<number, ResourceError>' is not assignable to type 'Either<number, ObjectNotFound>'.
.flatMap(n => f2(n))
.flatMap(n => f3(n));
const r = c.match(
(result: string) => result,
(err: ObjectNotFound) => err.msg
);
Link 到 playground.
由于不同的函数可能会产生不同类型的错误,因此 flatMap 链接中断。
我假设,从代码中可以清楚地看出总体意图。我希望我选择了正确的工具(monad)。
任何人都可以提出修复建议以使其全部正常工作吗?
[更新]: 感谢 SpencerPark 向正确的方向推动。 .match 按最初预期工作的完整实现如下(受 this post 启发):
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>;
match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R;
}
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> {
return new Left(this.error);
}
public match<R>(success: (result: TResult) => R, error: Matcher<TError, R>): R {
return (error as any)[this.error.type](this.error);
}
}
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> {
return f(this.value);
}
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) {}
}
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 f4(s: string): Either<number, NetworkError> {
return new Left(new NetworkError('someAdress'));
}
const c = f1('345')
.flatMap(n => f2(n))
.flatMap(n => f3(n))
.flatMap(s => f4(s));
const r = c.match(
(result: number) => result.toString(),
{
ObjectNotFound: (value: ObjectNotFound) => 'objectNotFound',
ResourceError: (value: ResourceError) => 'resourceError',
DivisionByZero: (value: DivisionByZero) => 'divisionByZero',
NetworkError: (value: NetworkError) => value.address
}
);
console.log(r);
更新后的游乐场是 here。
[UDPDATE 2]: 'catchall' 子句支持:
type DiscriminatedUnion<T> = { type: T }
type UnionOfNames<TUnion> = TUnion extends DiscriminatedUnion<string> ? TUnion["type"] : never
type UnionToMap<TUnion> = {
[NAME in UnionOfNames<TUnion>]: TUnion extends DiscriminatedUnion<NAME> ? TUnion : never
}
type Pattern<TResult, TMap> = {
[NAME in keyof TMap]: (value: TMap[NAME]) => TResult
} | ({
[NAME in keyof TMap]?: (value: TMap[NAME]) => TResult
} & { catchall: (value: DiscriminatedUnion<string>) => TResult})
// ... SKIPPED ...
const r1 = c.match(
(result: string) => result,
{
// when there is the 'catchall' case, others are optional. But still only the possible cases can be listed.
ObjectNotFound: (value: ObjectNotFound) => value.msg + value.timestamp,
catchall: (value: DiscriminatedElement<string>) => 'catchall ' + value.type
}
);
我们可以将flatMap
的签名改成如下:
flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;
这里的错误类型要么是前一次计算的错误,要么是return当前计算的错误。从概念上讲,这遵循 Either
模型。在您的示例中,链末尾的类型是预期的 Either<string, ObjectNotFound | ResourceError>
。 (修正看起来像是打字错误的内容后,f3(n: number)...
)。
整体计算可能 return 一个 ObjectNotFound
错误或 ResourceError
取决于失败的步骤。
最终匹配现在可以正确引发类型错误,因为 error
函数不处理 ResourceError
尽可能从 f2
.[=21= 引发的情况]