В какой-то момент мне надоело писать что-то типа 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);
Полезные ссылки:
- https://jrsinclair.com/articles/2016/marvellously-mysterious-javascript-maybe-monad/
- https://medium.com/@yyankowski/maybe-monad-in-javascript-to-save-us-from-the-hell-of-the-null-guard-clauses-bc9f9a1f291b
- https://github.com/stoeffel/awesome-fp-js
- https://tproger.ru/translations/better-javascript-code-with-fp-features/