Привет, меня зовут Марат Зимнуров и я тимлид в кросс-функциональной команде HR Admin Tech Авито. Наверняка, вы уже сталкивались с термином «функциональное программирование» (ФП). Если посмотреть на популярные доклады (например, на HolyJS), тема функционального программирования раскрыта довольно поверхностно: immutable-структуры, transitions — и всё. В русскоязычном сегменте нет нормального baseline-гайда, с которого можно начать, да и в англоязычном ситуация не лучше: пара разрозненных курсов и несколько докладов про иммутабельность и монады от хаскелиста.
Но проблема глубже. Многие до сих пор путаются в базовых концепциях: чистые функции, побочные эффекты, монады. ФП часто противопоставляют ООП — мы любим бинарное мышление. Но на самом деле это просто возможность взглянуть на мир программирования с другой стороны, погрузиться глубже материи. Пока может звучать пафосно, поэтому давайте разбираться.
В статье рассказываю о том, что такое функциональное программирование и почему вообще о нём стоит говорить.
Кстати, на изображениях внутри вы найдёте QR-коды с дополнительной информацией по теме.

О чём статья?
Математическая основа ФП
Функциональное программирование — это не просто какие-то приколы вокруг чистых функций, монад или иммутабельности. Важно иметь четкое понимание: как пользоваться этим инструментом, в каких случаях его применять, какова его история, откуда он происходит, к чему ведет и почему стал таким, какой есть.
Когда вы начинаете понимать истоки этой концепции, парадигмы, стиля — все становится проще. За ФП стоит мощная математическая концепция — теория категорий (теоркат в простонародье). Это сложный базис, который, к сожалению, не всем дается, — и это нормально. Математика после вышмата и дискретки была сложной, а тут еще сложнее.

Но давайте попробуем упростить это для нашего мира, где мы ежедневно программируем на JavaScript, TypeScript или любом другом языке. Как применить теоркат и запомнить его основы? Начнем с понятия функтора. Это базовая идея: у вас есть данные, и к ним можно применить функцию map — классическую функцию, которая проходит через каждый элемент и может что-то с ним сделать. Это немного напоминает Data-Oriented Programming, но идет дальше и глубже.

В результате вы получаете полезную схему, которую можно найти в интернете — её много обсуждали на Хабре и легко найти в Google. Я хочу дать вам практический инструмент.
Например, хотите понять, что такое монада?

Сначала разберитесь с функтором — это набор данных, который где-то хранится (вычисляется) и к которому применяется map.

Затем нужно понять apply — помните вопросы на собеседованиях про apply, bind, call и их различия? Вот мы добрались до apply-функтора. Что дальше?

А дальше у нас chain-функтор. Что такое chain? Цепочка, верно. Когда вы начинаете искать информацию в интернете, читать книги по математике — вы узнаете, что это способ, например, из сущности Пользователь извлечь Имя, просто сменив уровень абстракции.

Если совсем упростить — вы уже близки к пониманию монады. Но важно знать также истоки монад и происхождение идеи контекстов.
Теперь про инкапсуляцию. Лет пять назад на YT-канале ExtremeCode вышло видео про то, что «вы неправильно понимаете инкапсуляцию» — многие тогда действительно неправильно поняли и стали с пеной у рта доказывать, что инкапсуляция — это не сокрытие, именно потому, что насмотрелись таких вот роликов.
Так вот, всякий раз, когда вы хотите реализовать инкапсуляцию с сокрытием и методами реализации, когда хотите организовать доступ или что-то ограничить — вы можете превратить этот баланс в контекст и передавать его не только императивно внутри, но и как входной аргумент. Мы поговорим об этом позже. Технически здесь у нас контекст — или, если хотите, замыкание.

Теперь закрепим сказанное. Теория категорий в программировании — отличный фундамент, помогающий избежать изобретения велосипедов. Чаще всего мы создаём их просто потому, что не понимаем основу. Это нормальный процесс обучения. В повседневной работе используйте это как спецификацию. У вас есть схема: хотите реализовать концепцию или функтор — определите, что для этого нужно.

Таким образом, если кратко описать основные понятия, у вас есть базовое понятие функтора с функцией map, closure для создания контекста, applicative-функторы для операций над функциями-замыканиями и chain для распаковки данных. Теперь мы готовы определить монаду.
Монада
Можно изучать множество сложных концепций о том, что это единственный способ задания императивной последовательности в ФП. Но давайте проще, для запоминания: монада — это цепочка обязанностей, но на уровне типов и функции, или прям чтоб еще проще запомнить, но, упарываясь в философию, монада — это способ применить одно замыкание к другому (с методами bind для связывания и return для закидывания значений в функтор, что позволяет переводить вывод одной функции в другую).
За монадами стоят два подхода. Первый — философский, с неким божественным началом. Второй — математический, из теории категорий, где монада определяется как моноид в категории эндофункторов. Причём вы найдёте споры, где оба эти определения считаются неверными. И это нормально — концепции не высечены в камне, они постоянно эволюционируют и находят новое применение.
Посмотрим наглядный пример.

Слева bro, справа не bro. Just enough. Если код читается вертикально, как говорил Боб Мартин в «Чистом коде», при рефакторинге класса Date на 200 страниц, то это хорошо. Справа же — хаос. Если вы любите недетерминизм, отладку на работе, чтение кода по диагонали — вы возвращаетесь к концепции goto, вместо декларативщины. Безусловно, интересно читать книгу и постоянно перелистывать странички назад, но понятнее читать её последовательно.
Любые концепции помогают выстроить работу самым оптимальным способом под конкретный проект. В этом смысле, набор идей ФП, с которыми вы сталкиваетесь на собеседованиях, чётко определён, и это хорошо.

Он может выглядеть пугающе, и работать с ним поначалу непросто. Это нормально, хотя за этим скрывается много сложностей. Давайте их разберём.

Возьмём пример с морскими существами, которые, соединившись, считают себя графом. Это хорошая отправная точка для размышления: можем ли мы определить всё как простые структуры данных, построить из этих примитивов конечные автоматы?
Так мы приходим к теории автоматов — ещё одному разделу математики.

Автоматы Милли, автоматы Мура, абстрактные автоматы и прочие — всё это может казаться непонятным. И это нормально. В функциональном программировании вы, по сути, просто переключаете состояния. У вас есть набор функторов, с помощью которых вы преобразуете одну структуру данных в другую на уровне типов. Это работает. Переменные вам не нужны.
Но если вы хотите понять, почему всё устроено именно так, задайте себе вопрос: на чём вы пишете код? У вас может быть любой высокоуровневый язык.

Но думаете ли вы, что работаете с нулями и единицами? На этом обычно всё и заканчивается. Когда вас спрашивают «что такое программирование?», вы отвечаете: «Ну, я пишу что-то на каком-то языке, и потом это превращается в нули и единицы». Но куда понятнее объяснить обычному человеку так: «Я просто делаю так, чтобы мой код управлял электричеством».
И тут вы чувствуете что-то возвышенное — это нормально.
Когда вы задаёте себе этот вопрос, вы точно хотите думать об электричестве во время написания кода? Вот вы пишете какую-то модную, современную штуку, а она не работает так надёжно, как хотелось бы, и электричество расходуется впустую. Давайте вспомним, с чего всё начиналось.
Эволюция кода

Были цифры от персидского математика Аль-Хорезми. Эти цифры сами по себе ничего не хранят — мы просто пишем их на бумаге, они выполняют декларативную функцию. Потом появился Аристотель с его силлогизмами, где «Сократ должен умереть». Эту историю многие слышали на курсе логики в университете. Затем появилась алгебра логики, которую разрабатывали более полутора тысяч лет — то, что мы называем сейчас булевой алгеброй. Она тоже ничего не хранит, это декларативный стиль. Потом Морзе создаёт телеграф, позволяющий передавать электрические сигналы по проводам. А затем происходит нечто удивительное: Тьюринг создаёт первый компьютер с хранимой в памяти и исполняемой программой.

Как же появилось само понятие хранения данных? И правильным ли путём мы пошли, создавая современную разработку? Когда вы поймёте историю и основы теории категорий, вы сможете связать всё воедино и выработать более структурированное мышление. Давайте углубимся в историю.
У Тьюринга был супервайзер. Супервайзер — потому что он осуществлял надзор, supervision, то есть видел что-то большее. И часто ученики перенимают идеи своих учителей и научных руководителей. Так было и с Тьюрингом. Его руководителем был Алонзо Чёрч. Имена можно не запоминать, важно знать: есть Тьюринг и есть Чёрч. Что же сделали эти два гения, внёсшие огромный вклад в computer science и информатику?
Если упростить их работу, они пытались доказать, что бесконечные задачи, скорее всего, не решаются механическим путём, а конечные — решаются. Существуют разные формулировки этих тезисов: есть сложный тезис Чёрча-Тьюринга, есть упрощённый, который сформулировали другие, есть понятие полноты по Тьюрингу.
Что это значит для повседневной разработки? Чёрч создал основы декларативного программирования — то, с чем мы сталкиваемся сегодня, когда описываем поведение машины в YAML-файлах. А Тьюринг работал над концепцией бесконечности — это нечто, что может быть детерминированным или недетерминированным. Если мир недетерминирован, бесконечность, вероятно, существует. А если он детерминирован? Тут можно долго философствовать.

Сравнивая императивный и декларативный подходы, возьмём HTML и CSS — что из них язык программирования? Это популярный предмет для споров. Некоторые обсуждают эту тему. Есть те, кто считает HTML языком программирования — и это нормально, давайте разберёмся. CSS действительно является языком программирования. Но почему? Задумайтесь, как вы определяете, что одно — язык программирования, а другое — нет? Здесь важна концепция полноты по Тьюрингу из императивного мира. Чёрч создал концепцию типизированной и нетипизированной лямбды. Мы её разберём позже, не волнуйтесь. HTML имеет определённый набор термов (типов) — это как конструктор с определёнными деталями, из которых можно собрать статичную структуру. CSS тоже имеет набор термов, но может создавать циклы, бесконечные последовательности или прерывания — это и есть, в нашем случае, полнота по Тьюрингу.
Оба инструмента являются типизированными лямбдами, потому что имеют фиксированный набор ключей. JavaScript — это нетипизированная лямбда, позволяющая создавать неограниченные структуры данных, а TypeScript — скорее типизированная лямбда. На HTML можно создать детерминированный конечный автомат: настроить множество файлов и связать их условными переходами через гиперссылки, даже создать рекурсию с помощью iframe. CSS полон по Тьюрингу — это язык общего назначения.
Добавьте немного синтаксического сахара, и вы сможете делать на нём что угодно. С HTML ситуация иная: он не работает автономно в одном файле, вы не можете реализовать в нём полноценное императивное программирование с циклами, условиями и хранением переменных.
Декларативный подход
Давайте разберемся с декларативным подходом и его местом в компьютерных науках.

Чёрч создал концепцию лямбда-исчисления. Чёрч разработал так называемый «карандашно-бумажный метод» — программирование, которое работает на бумаге. Это фундамент функционального программирования. Чтобы понять ФП, нужно сначала разобраться в лямбда-исчислении, лямбда-выражениях и анонимных функциях — по сути, это всё, что пишется без имён.

Углубимся в более понятные определения. Когда мы говорим о ФП против объектно-ориентированного программирования, возникает вопрос: что является примитивом? В ФП примитив — это функция, но как же тогда работать с числами? Чёрч предложил элегантное решение. Числа не нужно хранить в компьютере. Когда вы пишете 2 + 2 на бумаге, бумага просто отображает выражение, но не хранит его — она как экран.
Числа можно реализовать через функции. Достаточно определить 0, единицу и несколько правил. Вызывая функцию определённое количество раз, вы получаете числа: один вызов — число 1, ещё вызов — число 2, и так далее. Числа существуют в рантайме, их не нужно записывать в константы.

А как проводить математические операции? Сложение, деление, умножение? Всё решается вложением функций друг в друга (монады, привет!). Это похоже на школьную математику с функциями f(x) и g(x), которые многим казались непонятными.

Теперь о логике. Как построить логические операции, когда у вас есть только функции? Как получить true и false? Решение элегантное: создаём функтор с двумя аргументами, x и y. Для true возвращаем x, для false — y. Так мы вычисляем значения на лету.
А как быть с циклами из императивного программирования? В ФП их заменяет рекурсия. Но тут возникает проблема: для рекурсии нужно имя функции, а в чистом ФП имён нет. Решение нашлось в Y-комбинаторе — математической конструкции, которая позволяет реализовать рекурсию без именованных ссылок.

Возьмём классический пример — вычисление факториала. Y-комбинатор можно определить в удобном стиле, реализовать функцию факториала без имён, и всё работает. Для простоты используются единицы, чтобы показать принцип работы факториала.
По ссылке множество примеров перформанса этого решения.

Допустим, вы готовы пойти дальше и реализовывать что-то «пострашнее». На сайте ИТМО есть отличные примеры, включая реализацию факториала в «ниндзя-стиле» — всё в одну строку. Lambda Calculus. Это работает на бумаге, как и задумывал Чёрч. Если в конце поставить число — реализуется алгоритм вычисления.
Можно преобразовать этот код в более читаемый вертикальный формат с типизированной лямбдой.

Для тех, кому сложно работать с чистыми числами, можно использовать другие структуры данных, например, списки.
Комбинируя теорию категорий с лямбда-исчислением, вы создаёте практические механизмы.

Все решения в итоге превращаются в HTML для конкретных задач. Нетипизированная лямбда даже превосходит полноту по Тьюрингу — на ней можно реализовать практически любую задачу.

Условия применения ФП на практике
Когда же мы применяем ФП на практике? Обычно при рефакторинге, когда код становится «грязным» и нужны чистые функции. Почему они называются «чистыми»? Не потому что они красивые (иначе они назывались бы «beauty/beautiful functions»).
Что же такое чистая функция? Это функция, которая не имеет побочных эффектов и не зависит от контекста? На первый взгляд, это может показаться сложным, ведь и у нее есть своя матчасть, но на самом деле всё гораздо проще. Чистая функция — это та функция, которая сохраняет ссылочную прозрачность для всех аргументов х, которые сохраняют ссылочную прозрачность.

Давайте рассмотрим два примера. Первый — это функция, которая вычисляет сумму двух чисел, а второй — функцию, которая возвращает строку. Второй пример сохраняет ссылочную прозрачность. Оба эти примера можно считать чистыми функциями, но есть одно важное отличие: первый пример зависит от места вызова, а второй — нет.
Если мы переместим первый пример в другое место и применим к нему параллелизм, вывод будет разным. Со вторым примером такого не произойдёт. Это позволяет легко переносить код и применять его в разных местах.
Однако, при рефакторинге возникает вопрос: стоит ли выносить сумму двух чисел в отдельный литерал? Ответ на этот вопрос зависит от логики программы. Если логика хорошо продумана и контексты не меняются, то можно оставить сумму в сумме. Но если логика не соответствует новым условиям, то лучше её вынести.
Функциональное программирование можно сравнить с искусством композиции. Когда вы пишете музыку, вы перекладываете ноты, соблюдая определённые правила. В результате получается гармоничное произведение. Но если правила нарушаются, то вы можете получить какофонию.



Ещё один пример — строительство дома. Вы можете заложить фундамент, подождать подходящий сезон, построить стены, установить крышу и только потом начать отделку. В этой аналогии функциональный подход позволяет легко переключаться между этапами строительства, не привязываясь к конкретным срокам и условиям.
Вы можете создать три отдельных фабрики: одна будет производить крышу с высокой скоростью, другая — фундамент, а третья — стены. Затем вы сможете комбинировать эти элементы в любом удобном для вас порядке. Если процесс не будет работать должным образом, например, если фундамент не будет положен на крышу, это вызовет проблемы.

Однако в реальности мы сталкиваемся с бизнес-требованиями и ограничениями ресурсов. Вам кто-то что-то мешает делать. Даже такой простой процесс, как код-ревью, может замедлять работу.

Теперь представьте, что вас спрашивают: «Что вы вообще делаете каждый день на работе?». Ответ звучит примерно так: «Мы принимаем запрос от клиента, обрабатываем его, сохраняем куда-то через что-то...» Много абстракций, не так ли?
Почему так происходит? Потому что ФП — это не типизированные лямбды. Это больше, чем типизация, больше, чем Тьюринг-полнота. Это подход, который позволяет решать практически любую задачу.
Если задуматься, в архитектуре разработки существует идея event driven design и event driven development. Но когда вы работаете, скажем, с JavaScript, у вас нет полноценного multi-threading. Это реальность.

А как бы выглядел EDD с обработкой событий, теоркатом и lambda calculus?
Вспомните, как в CS 1.6 мы подключались к игре: открыли список серверов, выбрали, подключились — всё, вы уже в моменте, играете. Сейчас же в современных играх вроде CS:GO добавили систему очередей: вы ждёте, пока сервер подберёт подходящий матч.
Но зачем усложнять? В 1.6 вы просто бегаете, стреляете, а сервер при запросе возвращает информацию: «Ну что, молодец ты или нет?». Этот подход похож на функциональную парадигму: нет императивного запроса и мгновенного ответа, но логика сохранена, и результат всегда будет.
Логика остаётся прежней. Вам не нужно мгновенно получать обратную связь. Вы её в жизни не получаете. У вас есть только delay — возможность отправить запрос, но не получить ответ. Что-то всегда возникает между запросом и ответом.
Мы пытаемся сделать процесс похожим на реальность, но не учитываем, что иногда мы его не видим.

Мы можем усложнить эту идею. У нас нет системы очередей, мы просто делаем пару выстрелов, и кто-то рандомно на карте получает хэдшот.
Что объединяет эти два способа применения ФП, теорката и lambda calculus идей к обычной игре, мультиплеер-клиент-сервер или peer-to-peer, как хотите? У вас есть гарантия исполнения: и в том и другом случае вы получите какой-то ответ. Как его отследить?

Для этого существуют паттерны. Есть такая вещь, как distribution logging — распределённое логирование. Если говорить просто, вы присваиваете каждому шагу (экшену) уникальный идентификатор. В Redux вы привыкли присваивать разные экшены: экшентайпы, экшенеймы, как хотите. Но если вы используете один UID, то теперь ивент, отправившийся, к примеру, в HR-тех: кандидат пришёл на интервью, загрузил какие-то документы, CV, кандидата пригласили на техническое интервью, он его прошёл, не прошёл, по нему дали фидбек, затем он там онбордается, загружает свои доки и так далее.
Как это реализовать? Вам нужно множество отдельных шагов, где вы присваиваете уникальный идентификатор с первого шага на второй. Если что-то сломается, вы просто пойдёте искать по этому идентификатору. Вы найдёте последнее место, где он отвалился, и пойдёте туда. А если процесс хорошо скомпозирован и декомпозирован, то вы ещё найдёте почему. Вам не нужно больше сидеть и дебажить длинную портянку, все данные у вас уже есть.

Как ФП упрощает жизнь
ФП помогает упростить процессы. Например, AWS Lambda взяли простую идею: каждая функция — это отдельный Node.js процесс. Такой подход позволяет решать задачи, следуя теории минимальных частей.
Теперь ваш бэкенд состоит из множества маленьких функторов. Проверка их работы не требует тестирования всей системы целиком: достаточно протестировать каждую функцию в отдельности.
Вот почему компании вроде Сбера, МТС и Яндекса внедряют эту систему (Functions): они упрощают тестирование, позволяют быстро адаптироваться и облегчают масштабирование.

Раньше мы использовали громоздкие подходы к тестированию: полный coverage и фреймворки/библиотеки вроде Cucumber, Cypress или Playwright. Это требовало времени, ресурсов и инфраструктуры, хотя можно написать кучу небольших юнит-тестов.
Интересный подход предложил Дэн Абрамов на конференции We Are Developers. В его выступлении читалась мысль, что, вероятно, мы прошли круг в 30 лет и, создав server components, снова сделали PHP. Не знаю, закладывал ли он эту идею и писал ли об этом кто-то, но покопавшись в sandbox я понял одно: серверные компоненты позволяют обмениваться функциями.
Например, вместо передачи JSON с интерфейсом для разных устройств вы можете отправить функцию, которая адаптирует интерфейс под конкретные условия: больше данных — больше пространства, меньше данных — меньше пространства, если end добавится пагинация. Это новый уровень декларативности, где функциональные идеи легко зашиваются в код.
React — это тот самый HTML, который стал языком программирования, где элементы визуально представляют собой функции, описанные names. Вы компонуете и декомпонуете интерфейсы, следуя простой идее: «Сделай одно или другое». Разумеется сохраняя базовые принципы.

Некоторые увлекаются и, например, злоупотребляют вложенными вызовами, что затрудняет читаемость. Такие случаи требуют рефакторинга, но это не делает функциональное программирование плохим — это лишь свидетельствует о том, что кто-то фанился.
А где реальность? Вам надо написать лямбда бекенд. Весь ваш бэк — один микросервис это один метод хендлер, которому передается, например, базовый контекст, которым вы оперируете.

Что происходит на 14 строке? Вы что-то где-то отрабатываете. Вы получили dynamo в виде базы, имя таблицы, идентификатор события — и поехали с ним работать. Либо пользователь что-то запросил, либо он что-то сделал.

И вот ваш функтор. Это просто последовательность. Вы просто отрабатываете.
Если вы будете читать этот текст: мне надо порезать полезную нагрузку, создать ее получение, отправить в базу, вытащить оттуда элемент, апстрингифайить и получить валидный статус. А если что-то пойдет не так — давайте вернем негативный статус. Все. На любом из этих шагов.

Под капотом, если написать это понятным языком, будет примерно так (если не сжимать в один редуктор). Вы получите следующую логику: одна функция что-то вернула, оно же передалось в другую систему. Если вы обратите внимание, справа от нейма нетипичная идея, как объявлять тип вашей функции. Если вы обычно пишете каждому аргументу какой-нибудь тип и что он возвращает, здесь мы пишем, что такое функция.

Это всё потому что в какой-то момент код превращается в Haskell или Rust. Где все, чем вы занимаетесь — TDD (type driven development). Все, что вы делаете — это пишете типы.
Ваша большая часть кода будет типизированной лямбдой, если вы захотите ее применять в каком то домене. Если вы можете увлекаться — можете писать как хотите.

То же самое: вы просто берете какие-то данные от пользователя, ничего никуда не сохраняя, вычисляете их на лету. Потому что компьютер от слова «вычислять» — compute. Это не калькулятор, не сохранятор. Даже в двоичный сумматор вы не можете что-то сохранить. Вы просто вычисляете. Это дорого с точки зрения перформанса на данный момент. Но в определенных пиках вы можете достигнуть хороших показателей и результатов.

Вам больше не нужно ничего, кроме точки входа и точки выхода. Вы работаете, грубо говоря, со стримом. Получаете какой-то результат — вычисляете.

Вы позволяете падать вашим сервисам (даже если сервисы дохнут как мухи), потому что можете на каждом этапе теперь это отхендлить, потому что у вас есть всякие идеи типа EDD, distribution logging. Вы можете обмениваться базой данной с клиента на сервере — получите прикольную идею DEFI.
Здесь собрана огромная портянка разных JS методов. Можете их почитать, больше залезть в историю, практические примеры, что-то копипастить.
Итоги

Итак, вы должны задать себе финальный вопрос. А что такое ФП?
С самого начала что-то было про чистые функции, непонятные монады. На выходе ФП — это нечто большее, нежели просто принцип программирования. Это огромный инструмент, который тянется с нами с появления чисел от Хорезми, аристотелевскими силлогизмами и который в конечном итоге превратился в мощный фундамент, где приятно работать просто потому, что вы можете почитать по нему книгу, написанную не программистом. Можете прочитать книгу, написанную 1000 лет назад и выкупить эти идеи.
Что думаете о функциональном программировании? Пишите в комментариях.
А если хотите вместе с нами помогать людям и бизнесу через технологии — присоединяйтесь к командам. Свежие вакансии есть на нашем карьерном сайте.
Комментарии (23)
asatost
11.07.2025 15:38Очередная статья на хабре на тему "Что такое ФП". Если честно, ни из одной из них я так и не понял, что такое ФП и чем оно принципиально отличается от императивного foo(bar(arg)) или, как вариант, ООП-шного getFoo().bar().baz().
В русскоязычном сегменте нет нормального baseline-гайда
Потому что в русском языке нет слова baseline?
Dhwtj
11.07.2025 15:38императивного foo(bar(arg))
Почему ты решил, что оно императивное?)
asatost
11.07.2025 15:38Почему ты решил, что оно императивное?)
Потому что GOSUB в Бейсике был уже лет 40 назад.
aamonster
11.07.2025 15:38Ага, и те же 40 лет назад в бейсиковских подпрограммах не было способа вернуть результат :-). Всё, что можно – это изменять (мутировать) состояние. Даже стека для локальных переменных не было – только для адресов возврата.
ApxuTechTop
11.07.2025 15:38Реально, очередная статья рассказывающая сложно о простом в перемешку с каким то бредом. Люди придумывают себе концепции чтобы загонять себя в их рамки и решать проблемы которые они же и создают. Вместо foo(bar(arg)) или по нормальному const b = bar(arg); const f = foo(b) они хотят писать arg.map(bar).map(foo). Для разных задач лучше подходят разные концепции, функциональщина действительно хорошо подходит для работы с потоками данных. Также не стоит забывать что для использования функциональщины в не предназначенных для этого языках необходимо сначала написать вспомогательный код, который накладывает дополнительные расходы и может оказаться трудно поддерживаемым. В общем, нормально делай нормально будет
CrazyOpossum
11.07.2025 15:38Оба неправы, ФП - это не map/reduce/filter, и операторы через точку; автор пишет как раз о правильных вещах. Один из аспектов ФП - это найти в поведении данных математику и, доказав её, защититься от связанных ошибок. А ещё от ФП до ЯОП один шаг.
Также не стоит забывать что для использования функциональщины в не предназначенных для этого языках необходимо сначала написать вспомогательный код, который накладывает дополнительные расходы и может оказаться трудно поддерживаемым.
Логично, кэп, но в предназначенных языках код поддерживается гораздо легче, чем какое-нибудь DDDшное поделие.
pilot114
11.07.2025 15:38Что думаете о функциональном программировании?
После таких статей стараюсь о нём не думать =)
ФП противопоставляют ООП не просто так - в ООП меньше концепций. Да, они не так глубоки и универсальны. Постоянно то тут то там всплывают досадные неудобства - то null обработать, то временную переменную завести, то хочется скомбинировать функционал, который почему-то не спроектирован как комбинирующийся. НО. Это буквально вопрос эстетики, мир не рушиться от этих именно что не удобств. Со временем ООП-макака становиться мудрой ООП-макакой и заранее знает как аккуратно нивелировать эти неудобства, оставаясь в приятных рамках всё того же небольшого набора концепций.
От ФП Троггу грустно. ФП Троггу давать молоток для всего с алмазным напылением с большая инструкция на 500 страниц. ООП давать Троггу молоток, отвёртка, микроскоп. Всё подходит забить гвоздь!
А если серьёзно, ну давайте лучше с процедурным программированием сравним и ответим честно на 2 вопроса: что нельзя написать процедурно? В чём отличие, кроме "красоты"?
Хотите красоты - делаете DSL, не пытаясь решать сразу все проблемы мираavost
11.07.2025 15:38что нельзя написать процедурно?
... и на ассемблере, ага. Дальше продолжать?
В чём отличие, кроме "красоты"?
В издержках. И компромисах.
pilot114
11.07.2025 15:38Ассемблер - отличный пример. Есть места где ассемблер будет просто незаменим в практическом смысле. Незаменимого ФП я придумать не могу.
Ну а издержки и компромиссы буквально везде есть. Небольшой доп.объём кода я переживу, а вот ментальную модель без переменных но с необъяснимыми монадами - спасибо, но нет.
IUIUIUIUIUIUIUI
11.07.2025 15:38А если серьёзно, ну давайте лучше с процедурным программированием сравним и ответим честно на 2 вопроса: что нельзя написать процедурно?
Любой алгоритм можно написать процедурно. И λ-исчисление, и машина Тьюринга, собсна, Тьюринг-полны.
В чём отличие, кроме "красоты"?
Отличие в том, какую информацию о функции даёт её тип, и, соответственно, что нельзя написать (а я утверждаю, что современное ФП — это не про «передачу функции в функции», что умеют почти все языки, а про выражение семантики в типах).
Если у меня в хаскеле написано
foo :: Int → String
, то я знаю (при некоторых дополнительных не важных для принципа вводных), что эта функция не общается с внешним миром, не отправляет запрос к БД, не читает и не пишет в глобальные переменные, и не отправляет мой~/.bitcoin/wallet
на сервер злоумышленника. Если у меня в плюсах написаноstd::string foo(int)
, то эта функция может делать всё что угодно.Соответственно, рефакторить типизированный код куда проще и одно удовольствие. В моей практике хаскеля там действительно работает «if it compiles after refactoring, it works», в моей практике плюсов это не работает почти никогда.
Короче, отличие в том, что о коде проще рассуждать, и компилятор проверяет больше вещей.
Хотите красоты - делаете DSL, не пытаясь решать сразу все проблемы мира
Кстати, и eDSL, и DSL на хаскеле и подобных языках делать — одно удовольствие.
pilot114
11.07.2025 15:38Вот это хорошее объяснение, мотивирует в 3 раз попытаться разобраться с Haskell =)
Справедливости ради, типизация везде дает эффект лучшей предсказуемости. Но не настолько крутой, чтобы вообще не писать тесты. Кстати, как выглядят тесты на Haskell, они вообще бывают нужны?
IUIUIUIUIUIUIUI
11.07.2025 15:38Справедливости ради, типизация везде дает эффект лучшей предсказуемости. Но не настолько крутой, чтобы вообще не писать тесты. Кстати, как выглядят тесты на Haskell, они вообще бывают нужны?
Да, особенно если речь о взаимодействии с внешним миром и/или об интеграционных тестах.
Для внутренней, чистой логики их нужно сильно меньше, потому что много чего ловится типами. Хаскель же по большому счёту популяризовал property-based testing, и, например, либа quickcheck — про них. Вы пишете выражение вроде
\list → reverse (reverse list) === list
, а либа сама генерирует примеры, проверяет выражение, и в случае нахождения контрпримера пытается его уменьшить. Где-то рядом model-based-тесты, где у вас есть медленная, но легко понимаемая и проверяемая модель вашей логики, и есть оптимизированная, но тяжёло проверяемая. Вы аналогично пишете prop-тесты, что их результаты совпадают.Чтобы тесты (особенно property-based) на внутреннюю логику не писать и довериться тайпчекеру на все 100%, нужны ещё более сильные системы типов, вроде идриса или агды.
ИМХО хорошим ресурсом на тему будет книжка Сэнди Магвайра «Algebra-driven design». Там мало формальной теории (и её можно скипать), но идеи она передаёт выразимые и в более мейнстримных языках.
rukhi7
11.07.2025 15:38Слева bro, справа не bro. Just enough. Если код читается вертикально...
А если горизонтально?
Я придумал новый язык! Там код читается по диагонали, это новое слово в программировании, я обещаю... через несколько лет... ну может через пару десятков лет, увидите, все перейдут на диагональное программирование!
Dhwtj
Функциональное программирование основано на отрицании времени.
Только возможные факты и правила их соответствия. Правила дают гарантии ещё до запуска программы.
Когда эта мантра будет усвоена дальнейшее изучение ФП станет легче
Из практических соображений имеет смысл не отрицание (как в Haskell), а вытеснение его наружу в эффектные слои, которые соприкасаются с миром, выполняют конкретные use cases, привязанные к контексту. И всё же, мы стараемся проверить правила ещё до запуска, ведь чем позже проверка, тем дороже ошибка, поэтому максимально «заталкиваем» проверки вверх по цепочке, остальное страхуем тестами и мониторингом.
Императивное программирование можно противопоставить ему. Ещё нет правил, но уже есть желание дойти до цели. Умный командир, глупые исполнители. Такой стиль провоцирует что командир занимается не своим делом, а разбирает проблемы исполнителей на лету в зависимости от контекста и без гарантий.
ФП основано на чистых функциях, которые соединяют выход и вход. Они ничего не меняют: внешний контекст (файлы и прочий IO), состояние (например, члены класса) и не кидают исключений. Такие функции легко композировать.
IUIUIUIUIUIUIUI
Так хаскель не отрицает. Хаскель как раз за счёт системы типов проверяет, что всё нормально раскинуто либо в чистые, либо в
CrazyOpossum
В Хаскеле всё красивее. Есть тривиальная монада State, которая позволяет читать и писать контекст. А Хаскельная IO, в которой крутятся эффекты - это просто State, с контекстом #RealWorld. То есть, ввод с клавиатуры, сигналы, вызовы fopen, fread, fwrite, время и прочее и прочее - это просто контекст.