Монады - это довольно полезный способ применения некоторых шаблонов в коде, которые (математически) гарантированно дадут вам определенные результаты.

Я трачу много времени на кодирование на JavaScript (бедняжка!) Для Node.js. За последние несколько лет я обнаружил, что последовательное использование обещаний может оказаться непростым делом. В особенности сейчас парадигма await/async в ES2017, вероятно, еще больше сбивает с толку вопрос о том, какой стиль программирования принять.

В недавнем проекте мне нужно было убедиться, что асинхронные события выполняются в определенном порядке, и если одно из них выйдет из строя, цепочка перестанет выполняться и сообщит об ошибке. Этот подход полезен, когда, например, бизнес-логика должна быть реализована внутри кода, т.е. требования нетехнические и несколько произвольные. Я написал глубоко уродливую смесь обещаний, возвращающих контейнеры Either. В процессе это подсказывает мне, что Either и Promise работают примерно одинаково.

Вот как «либо» выглядит в TypeScript:

class Either<Left, Right = undefined> {
  static of<Left, Right>(value: [Left] | [undefined, Right]): Either<Left, Right> {
    return new Either<Left, Right>(value);
  }
static Right = function<Right>(value: Right) {
    return new Either<undefined, Right>([undefined, value]);
  };
static Left = function<Left>(value: Left) {
    return new Either<Left, undefined>([value]);
  };
private value: [Left] | [undefined, Right];
constructor(value: [Left] | [undefined, Right]) {
    this.value = value;
  }
map<T>(func: (val: Right) => T): this | Either<undefined, T> {
    return this.value.length === 1 ? this : Either.Right<T>(func(this.value[1]));
  }
flatMap<leftT, rightT>(
    func: (val: Right) => Either<leftT> | Either<undefined, rightT>
  ): this | Either<leftT> | Either<undefined, rightT> {
    return this.value.length === 1 ? this : func(this.value[1]);
  }
either(left: (val: Left) => any, right: (val: Right) => any) {
    if (this.value.length === 1) {
      return left(this.value[0]);
    } else {
      return right(this.value[1]);
    }
  }
}

ВНИМАНИЕ: Хотя приведенный выше код в порядке, желать лучшего с точки зрения статических типов не стоит. Более того, это не единственная возможная реализация, тем более, что внутренняя работа контейнера может быть выполнена иначе.

Самое замечательное в этой монаде то, что вы либо получаете значение или сообщение об ошибке в конце цепочки. Независимо от того, сколько вы сопоставите с «левым» значением, больше ничего не будет выполнено!

Either
  .Right(0)
  .flatMap( i=> i === 0 ? Either.Left("division by zero") : Either.Right(10/i))
  .map(i => i + 1)
  .either(
    left => {console.log(left); return undefined;},
    right => right
  )

Главный аргумент в пользу слияния Either и Promise заключается в том, что вызов Promise().then можно рассматривать как эквивалент Either.of().map. Более того, шаблон

Promise()
  .then(i => "do something")
  .catch(error => "something went wrong")

очень похож на

Either.of(...)
  .either(
    error => "something went wrong",
    i => "do something"
  )

В обоих случаях вы либо получаете значение, либо сообщение об ошибке. Почему бы не жениться на двоих?

Узрите PromisedEither! (работа в процессе)

class PromisedEither<A, B> {
 static Right = <B>(val: B) => {
    return new PromisedEither<undefined, B>(Promise.resolve(val));
  };
static Left = <A>(val: A) => {
    return new PromisedEither<A, undefined>(Promise.reject(val));
  };
private value: Promise<A | B>;
constructor(promise: Promise<A | B>) {
    this.value =  promise;
  }
map<C>(func: (val: B, resolve: (val: C) => C, reject: (val: A) => A) => void) {
    return new PromisedEither<A, C>(
      this.value.then(
        (val: B) => new Promise<C>((resolve, reject)  => func(val, resolve, reject)) 
      )
    );
  }
either(left: (val: A) => void, right: (val: B) => void): void {
    this.value
      .then(right)
      .catch(left);
  }
}

С этим малышом абсолютно законна следующая магия:

Either.Right("Good news")
  .chain(val => PromisedEither.Right(val))
  .map((val, resolve) => resolve(val + '. Excellent!'))
  .either(
    () => done(),
    right => {
      assert.equal(right, "Good news. Excellent!");
    }
   );

Заключение

Полное раскрытие информации, PromisedEither ЯВЛЯЕТСЯ синтаксическим сахаром в конце дня. Но он применяет определенный шаблон, который будет казаться естественным разработчикам, уже использующим mondas, и дает им структуру, а не пустой холст для заполнения любым кодом ...

Меня зовут Ник Ваклев, я основатель Techccino Ltd | Индивидуальные бизнес-приложения.