Контейнерные типы: Result, Maybe, Either, Sum
Контейнерный тип — обёртка, защищающая значение через внешний интерфейс. Контейнерные типы (Record, Tuple) и алгебраические типы (Maybe, Either, Result, Sum) — два разных понятия. Не все они монады в строгом смысле.
Контейнерные vs алгебраические
Это контейнерные типы. У нас есть специальный внешний интерфейс, который позволяет защищать значение, делать его иммутабельным.
Контейнерные типы — про защиту значения. Алгебраические — про моделирование альтернатив (Sum = "одно из").
Это не монады, они не реализуют map/apply полностью. Это контейнеры для значения.
Records & Tuples — отменённый proposal
Нам долгое время обещали records, делать их иммутабельными, ставить решётку перед объектами. Но этого не будет.
Решение — реализовать Record руками:
class Record {
static immutable(fields) { return Record.#build(fields, true); }
static mutable(fields) { return Record.#build(fields, false); }
static #build(fields, frozen) {
return class {
static create(...values()) {
const obj = Object.create(null);
for (let i = 0; i < fields.length; i++) {
obj[fields[i]] = values[i];
}
return frozen ? Object.freeze(obj) : obj;
}
};
}
}
const City = Record.immutable(['name']);
const User = Record.mutable(['id', 'name', 'city', 'email']);
const rim = City.create('Rim');
const mark = User.create(1, 'Marcus', rim, 'm@rome');
При помощи Record.immutable создаём класс с одним полем name. Mark mutable, но форма фиксирована — нельзя удалить поле.
Object.create(null) быстрее литерала
const obj = Object.create(null);
Объект без цепочки прототипного наследования. Несколько шагов поиска свойств исключены. Поэтому такие records работают даже быстрее объектного литерала.
fork и update
Record.fork(obj, { name: 'new' }); // копия с изменением
Record.update(obj, { name: 'new' }); // мутация (только mutable)
Update меняет поля у мутабельных. Fork создаёт копию и в ней меняет значение. Immutable обновляется только через fork.
Maybe — алгебраический тип
class Maybe {
static of(value) { return new Maybe(value); }
isNothing { return this._value == null; }
map(fn) { return this.isNothing ? this : Maybe.of(fn(this._value)); }
chain(fn) { return this.isNothing ? this : fn(this._value); }
getOrElse(d) { return this.isNothing ? d : this._value; }
}
Защищает от null/undefined в цепочке.
Either — Result
class Right { constructor(v) { this._v = v; } map(f) { return new Right(f(this._v)); } chain(f) { return f(this._v); } fold(_, on) { return on(this._v); } }
class Left { constructor(e) { this._e = e; } map { return this; } chain { return this; } fold(on, _) { return on(this._e); } }
const tryCatch = (fn) => {
try { return new Right(fn); }
catch (e) { return new Left(e.message); }
};
tryCatch( => JSON.parse(input))
.map(obj => obj.user)
.fold(
err => console.error('parse failed:', err),
val => console.log('user:', val)
);
Left "пропускает" дальнейшие map/chain — обработка ошибок без try/catch.
Sum type — "одно из"
// псевдо-схема через discriminator
const Event =
| { type: 'click', x: number, y: number }
| { type: 'key', code: string };
function handle(e) {
switch (e.type) {
case 'click': /* x, y известны */; break;
case 'key': /* code известен */; break;
}
}
В TS это полноценный discriminated union. В JS — через ручной discriminator.
Валидация через схемы и функции
Можно сделать функции, которые будут валидировать каждое значение — isEmail, isURL. По сути валидация схемами + валидация через функции + проверка по справочнику.
Зачем
- Защита от мутаций вложенных объектов
- Цепочки без проверок на null
- Errors as values вместо exceptions
- Чёткое моделирование "одно из" вариантов
- Документация типа через имя контейнера
Связанные темы
Ресурсы
- Лекция (RU): X4yZVNOoMEM
- Лекция (UA): 6gyt_s9r8To