简体   繁体   English

TypeScript 中的任一 monad + 模式匹配

[英]Either monad in TypeScript + pattern matching

I'm trying to implement Either monad in TypeScript.我正在尝试在 TypeScript 中实现任何一个 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 to playground .链接到操场

As different functions can potentially generate errors of different types, flatMap chaining breaks.由于不同的函数可能会产生不同类型的错误,因此 flatMap 链会中断。

I assume, overall intention is clear from the code.我认为,从代码中可以清楚地看出总体意图。 And I hope I've chosen the right tool (either monad).我希望我选择了正确的工具(monad)。

Can anyone suggest a fix to make it all work?任何人都可以建议修复以使其全部正常工作吗?

[UPDATE]: Thanks to SpencerPark for the nudge in right direction. [更新]:感谢SpencerPark在正确的方向上的推动。 The full implementation with .match working as originally intended is below (inspired by this post): .match 按原计划工作的完整实现如下(受本文启发):

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);

Updated playground is here .更新的游乐场在这里

[UDPDATE 2]: 'catchall' clause support: [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
  }
);

We can change the signature of flatMap to the following:我们可以将flatMap的签名更改为以下内容:

flatMap<R, E>(f: (value: TResult) => Either<R, E>): Either<R, E | TError>;

Here the error type is either the error from the previous computation or the one returned by the current one.这里的错误类型要么是前一次计算的错误,要么是当前计算返回的错误。 Conceptually that follows from what Either models.从概念上讲,这遵循Either模型。 The type at the end of the chain in your example is Either<string, ObjectNotFound | ResourceError>您的示例中链末尾的类型是Either<string, ObjectNotFound | ResourceError> Either<string, ObjectNotFound | ResourceError> as expected. Either<string, ObjectNotFound | ResourceError>正如预期的那样。 (After fixing what looks like a typo, f3(n: number)... ). (在修复了看起来像是打字错误的f3(n: number)... )。

The overall computation could return an ObjectNotFound error or ResourceError depending on what step failed.根据失败的步骤,整个计算可能会返回ObjectNotFound错误或ResourceError

The final match now properly raises a type error because the error function doesn't handle the case where a ResourceError is raised as possible from f2 .最终匹配现在正确地引发类型错误,因为error函数不处理从f2引发ResourceError的情况。

See the updated playground link查看更新的游乐场链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM