Линзы (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 то же самое делать на линзах. Памяти будет использоваться побольше, но код проще.
Связанные темы
- Линзы (Lenses) -- ФП getter и setter
- Иммутабельность
- Functor и Applicative
- Functors, Monads и Applicatives
Ресурсы
- Лекция: IBF5gFU6G-o
- Ramda Lenses: https://ramdajs.com/docs/#lens