IO монада и изоляция эффектов
IO — функция, оборачивающая побочный эффект. До вызова
.runэффект не происходит. Цепочка.mapи.chainстроит описание вычисления, эффект изолирован в одном месте. Идея — отложить I/O и сделать остальной код чистым.
Идея IO монады
В чистом ФП побочные эффекты (console.log, fs, fetch) запрещены. Чтобы сохранить чистоту цепочек, эффект оборачивают в IO-контейнер:
const createIO = (effect) => {
const run = () => effect;
run.map = (f) => createIO( => f(effect));
run.chain = (f) => createIO( => f(effect).run);
return run;
};
const readUser = createIO( => fs.readFileSync('user.json()', 'utf-8'));
const parsed = readUser.map(JSON.parse);
const upperName = parsed.map(u => u.name.toUpperCase());
// Никакого I/O ещё не было! Описание готово.
upperName.run; // только сейчас читается файл и происходит трансформация
map vs chain
| Метод | Когда |
|---|---|
.map(f) |
f возвращает обычное значение |
.chain(f) |
f возвращает другой IO (избегаем IO<IO<T>>) |
const readFile = (path) => createIO( => fs.readFileSync(path, 'utf-8'));
// map — даёт IO<IO<string>>, плохо
const result1 = readUser.map(name => readFile(name + '.txt'));
// chain — "схлопывает", даёт IO<string>
const result2 = readUser.chain(name => readFile(name + '.txt'));
Зачем
- Тестируемость: чистый код легко тестировать; IO-описание можно подменить mock-функцией
- Композиция: эффекты собираются в pipe/compose так же, как чистые значения
- Откладывание: одно и то же описание можно выполнить много раз
- Локализация: все эффекты в одном
.runна границе приложения
Изоляция через .run на границе
// Чистая часть программы — собирает описание
const program = pipe(
readUser,
parseJson,
validateUser,
saveToDb
);
// Граница (main, entry point) — единственное место с эффектами
program.run;
Примеси и дублирование — антипаттерн в IO
Можно и в таком стиле, на самом деле, писать с примесями. Только я не люблю примеси — дублируется всё-таки код.
автор про неудачный рефакторинг Cursor: модель добавила методы через примеси внутрь createIO, что породило дублирование логики run.
LLM плохо знает ФП на JS
Пока что я вижу, что LLM не очень с функциональным программированием на JS. Где ему было обучиться? Мало кто в таком стиле пишет.
Если задачки задавать на Haskell или ClojureScript — LLM справилась бы. На JavaScript ей просто негде научиться ФП.
Чисто функциональный JS — редкость в обучающих данных. LLM по умолчанию генерирует ООП-стиль с заигрыванием под функциональный.
IO для разных эффектов
const log = (msg) => createIO( => console.log(msg));
const random = createIO( => Math.random);
const now = createIO( => Date.now());
const ajax = (url) => createIO( => fetch(url));
Связь с Promise
Promise — частный случай монады, но "слишком умный": запускается сразу при создании. IO откладывает запуск. В ФП-языках для I/O используют именно lazy-структуры (Task, IO, Effect), не Promise.
Effect в реальном мире
Библиотеки:
- fp-ts (Task, IO, Either)
- Effect (новая, мощная) — https://effect.website
- Most.js, Bacon.js — для потоков
Связанные темы
- Functors, Monads и Applicatives
- Контейнерные типы Result Maybe Either
- Чистые функции
- Побочные эффекты
Ресурсы
- Лекция: Q4B2N9ZOX0A