Линзы (Lenses) — ФП getter и setter

Линза — пара функций (getter, setter), упакованных в один объект. Позволяет читать, заменять и трансформировать вложенные поля иммутабельных структур, всегда возвращая новый объект.

Минимальная реализация

const lens = (get, set) => ({ get, set });

const view = (lens, obj) => lens.get(obj);
const set = (lens, value, obj) => lens.set(value, obj);
const over = (lens, fn, obj) => lens.set(fn(lens.get(obj)), obj);

Объект инкапсулирует все поля, а линза инкапсулирует имена — что именно мы хотим смотреть.

Линза для одного поля

const lensProp = (key) => lens(
  (obj) => obj[key],
  (value, obj) => ({ ...obj, [key]: value })
);

const nameLens = lensProp('name');
const person = { name: 'Marcus', born: -121 };

view(nameLens, person);              // 'Marcus'
set(nameLens, 'Mark', person);       // { name: 'Mark', born: -121 }
over(nameLens, s => s.toUpperCase(), person); // { name: 'MARCUS', born: -121 }
// person НЕ изменился

set должен нам вернуть новый объект person — это уже будет не тот person, что мы передали, а абсолютно новый, в котором name установлен новым значением.

over: getter + map + setter

const over = (lens, fn, obj) =>
  lens.set(fn(lens.get(obj)), obj);

Линза клонирует объект, только делая одному полю map.

delete через деструктуризацию

const remover = (source) => (obj) => {
  const { [source]: forgot, ...other } = obj;
  return other;
};

Одно поле мы вычитываем из объекта по имени source. Это вычислимое имя поля. И оно нам нужно только для того, чтобы его забыть.

source/destination — копировать в другое поле

const lensField = (source, destination = source) => lens(
  (obj) => obj[source],
  (value, obj) => ({ ...obj, [destination]: value })
);

const nameToFull = lensField('name', 'fullName');

Композиция линз

Линзы можно компоновать для вложенных объектов:

const lensCompose = (outer, inner) => lens(
  (obj) => inner.get(outer.get(obj)),
  (value, obj) => outer.set(inner.set(value, outer.get(obj)), obj)
);

const addressLens = lensProp('address');
const cityLens = lensProp('city');
const userCityLens = lensCompose(addressLens, cityLens);

view(userCityLens, user);
set(userCityLens, 'Kyiv', user); // обновит nested поле

Зачем нужны линзы

  • Иммутабельные обновления вложенных структур без spread-лестницы
  • Альтернатива Immutable.js — пишутся за 10 строк
  • Переиспользуемые "указатели" на поля — nameLens работает на любом объекте с полем name
  • Базовый блок для Ramda/Lodash/fp-ts

Теперь вы можете использовать без библиотеки Immutable.js то же самое делать на линзах. Памяти будет использоваться побольше, но код проще.

Связанные темы

Ресурсы