Javascript и монада Maybe

· 2 минуты на чтение
Javascript и монада Maybe

В какой-то момент мне надоело писать что-то типа object && object.prop1 && object.prop1.prop2. Это ужасно и неправильно. Кроме очевидного варианта с перехватом исключения try/catch, нашлись опциональные цепочки, и, конечно же, монады.

В новых версиях JS появились опциональные цепочки ?., безопасный способ доступа к свойствам вложенных объектов, даже если какое-либо из промежуточных свойств не существует, то есть вместо user && user.address && user.address.streetможно написать user?.address?.street. Но в моем случае этот вариант не подошел, использовалась старая версия Node.js.

Во-первых, а что такое монада? Монада — это особый тип данных в функциональных языках программирования. Про функциональное программирование я только слышал и считал, что это не про Javascript, а оказалось, что даже на вики написано:

Поддерживает объектно-ориентированный, императивный и функциональный стили.

Вот про функциональный стиль Javascript мы сегодня и поговорим. Я не буду углубляться в определение что такое монада и цитировать умные книжки, а расскажу как я понял её сам. Монада — это что-то вроде контейнера, который поддерживает определенные стандартами операции. Самый первый стандарт, который находит Google — это Fantasy Land.

В общем и целом, монада должна поддерживать как минимум:

  • map
  • join
  • chain
  • ap

Это позволяет соединять различные (или даже одинаковые) монады в цепочки, так как все они следуют единому стандарту.

Есть куча готовых библиотек с различными готовыми монадами, но это не наш метод, чтобы разобраться как это работает — нужно написать свою версию монады Maybe. Ниже одна из реализаций класса Maybe, уверен, что не полная, а может быть и где-то неправильная, но моя.

class Maybe {
  constructor(val) {
      this.__value = val;
  }

  static of(val) {
      return new Maybe(val);
  }

  static Nothing() {
      return Maybe.of(null);
  }

  flatMap(fn){
      if(this.isNothing()) return Maybe.Nothing();
      const m = fn(this.__value);

      return m.isNothing() ? 
           Maybe.Nothing() : 
           Maybe.of(m.__value);
  }

  getOrElse(elseVal) {
      return this.isNothing() ? elseVal : this.__value;
  }

  getOrEmptyArray() {
      return this.getOrElse([]);
  }

  getOrNull() {
      return this.getOrElse(null);
  }

  isNothing() {
      return this.__value === null || this.__value === undefined;
  }

  map(fn) {  
      return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this.__value));
  }

  join() {
      return this.__value;
  }

  chain(fn) {
      return this.map(fn).join();
  }

  ap(someOtherMaybe) {
      return someOtherMaybe.map(this.__value);
  }
}

Ну и, по сути, вся проверка на существование свойства объекта происходит банальным this.__value === null || this.__value === undefined.
А теперь попробуем монаду Maybe в деле:

Подключим необходимые модули, для работы с path используется библиотека rambda:

import { path } from 'rambda'

Определяем объект testObj:

const testObj = {
  "a": {
      "b": {
          "c": "test"
      }
  }
}

А затем создаем экземпляр нашего класса Maybe.

const maybeObj = Maybe.of(testObj)

Для того, чтобы получить строку, используется функция join().
А здесь мы пытаемся получить значение свойства c:

const val = maybeObj
  .map(path(["a", "b", "c"]))
  .join()

Далее попытка обратиться к несуществующему свойству не приведет к возникновению ошибки, а просто вернет undefined.

Полный исходный код ниже:

import { path } from 'rambda'

const testObj = {
    "a": {
        "b": {
            "c": "test"
        }
    }
}

const maybeObj = Maybe.of(testObj);
console.log(maybeObj);
console.log(maybeObj.join());

const val = maybeObj
    .map(path(["a", "b", "c"]))
    .join();
console.log(val);

const valNothing = maybeObj
    .map(path(["test", "b", "c"]))
    .join();
console.log(valNothing);

Полезные ссылки: