TL;DR: Затем, что с ним код чище, читаемее и предсказуемее ;)
Старый объектно-ориентированный или императивный подход к программированию несёт в себе множество проблем, которые решает функциональное программирование. Даже в современной среде все до сих пор считают, что объектно-ориентированное программирование — правильное программирование, а функциональное — «для математиков и задротов», или вообще даже для варваров, которые даже не слышали об объектной и императивной «цивилизации».
Начиная с 90-х, у индустрии появился фетиш на классы, объекты, иерархии, наследование, инкапсуляцию, полиморфизм и прочие тяжкие грехи программирования. Расплодилось великое множество объектно-ориентированных языков: C++, Java, C#, JavaScript, Python, Ruby, Kotlin, Swift, TypeScript — и это только вершина айсберга. Вузы преподают только ООП. Всё остальное считают «странным» или «непрактичным». Выпускники выходят в индустрию с убеждением, что классы и наследование — это единственный путь. И они несут эту веру дальше, создавая новые и новые слои императивного ада.
В рамках «Функционального Rust» я не буду учить вас основам Rust или слишком глубоко зарываться в историю функционального программирования. Для этого есть официальная The Book, сотни вебинаров, документации, статьи, можете даже спросить ChatGPT. Моя цель — изменить ваш подход. Я покажу, как писать на Rust функционально: иммутабельно, декларативно, с композицией, без циклов и мутаций там, где можно обойтись без них. Я даже расскажу, где функциональный стиль не нужен, но таких мест гораздо меньше, чем принято думать.
Императивный или объектно-ориентированный подход к программированию несёт в себе множество проблем, и вот ключевые из них:
Проблема I: Мутабельность.
Представьте, что вы купили квартиру в старом доме. Ей 150 лет. Вы вкладываете душу и деньги: делаете дизайнерский ремонт, ставите мебель, которой позавидовал бы сам Артемий Лебедев, монтируете кухню с посудомойкой и индукционной плитой, вешаете водонагреватель, чтобы в июне не бегать на общую кухню за тёплой водой. Теперь внутри всё блестит и радует глаз.
Но это всё та же 150-летняя квартира.
Проводка — старая, алюминиевая, ещё с прошлого века. Канализация — ржавая, забитая. Водопровод — трубы, которые помнят ещё царя. Вода из крана — жёсткая, с тяжёлыми металлами. Мыться в ней — тот ещё «кайф». Подключить мощную технику — лотерея, ставка в которой — вся квартира. Стены потихоньку осыпаются. Дом могут признать аварийным в любой момент, а потом и снести.
// Императивный мутабельный Rust fn my_function(num: i32) -> i32 { let mut x = num; // Хватит // ...ещё 30 строк... x += 96; // Мутировать // ...ещё 50 строк... x // Значения }
Что здесь не так? Вы не создаёте новое значение — вы портите старое. Вы не знаете, что в итоге получится, потому что между строк что-то могло изменить x ещё раз. Код становится грязным, непредсказуемым, хрупким. Как старая проводка — одно неловкое движение, и короткое замыкание с ароматом плавленого пластика обеспечено.
Решение — иммутабельность.
Вы продаёте эту квартиру. И переезжаете в новый дом. С современной проводкой, пластиковыми трубами, фильтрами для воды. Построенный по всем стандартам качества. Где не нужно бояться, что замыкание спалит всю технику. Где вода не пахнет железом. Где дом не снесут через год.
Покупка новой квартиры — создание нового значения вместо мутирования старого. Как покупка новой машины вместо бесконечного колхоза своего 40-летнего «Жигуля». В Rust это очень хорошо продумано с помощью семантики перемещения и иммутабельности по умолчанию:
// Иммутабельность в Rust let x = 10; let new_x = process(x); // x переехал и больше не висит в памяти! // Значение просто переехало в другую квартиру.
В Rust вы не можете просто взять и поменять любое значение. Его нужно явно пометить как мутабельное с помощью ключевого слова mut.
let x = 10; // x = 20; // НЕТ! let mut y = 10; y = 20; // Разрешено
Но даже с этими знаниями, вместо того, чтобы бездумно мутировать каждое значение, остановитесь и подумайте:
Нужна ли вам мутабельность? Есть ли способ это сделать иммутабельно?
Если нет, перечитайте пункт 1.
Если уж совсем никак — мутируйте, но не позволяйте мутабельности распространяться на весь код. Изолируйте её в рамках одного блока.
Проблема II: Императивность
Некоторые до сих пор считают, что Rust — это «всего лишь C++ с проверками», и они ошибаются. Эти люди недооценивают потенциал Rust, обращаясь с ним по-старому, консервативно, раздувая и уродуя код. Представьте, что вы сидите с компьютером в баре за столиком. Вам захотелось ещё пива, но вам лень вставать, и в императивном программировании вы бы объяснили компьютеру путь до барной стойки:
// Императивный Rust computer.forward(50); computer.right(90); computer.forward(30); let beer = computer.ask("beer"); computer.grab(beer); // ...и так далее... // Куча строк. Зачем? Я просто хочу пива.
Решение — декларативность.
Вы просто говорите компьютеру «принеси мне пива»:
// Декларативный Rust — говорим «ЧТО». let beer = computer.bring_beer();
Суть декларативного синтаксиса в том, что вы описываете, ЧТО вы хотите, а не КАК это сделать. Программа сама разберётся, как получить требуемый результат, вам нужно лишь сказать, ЧТО вы хотите.
// Императивный Rust let mut squares = Vec::new(); // Мутабельно. for num in 0..10 { // Водим программу за ручку. squares.push(num * num); // Мутируем. } println!("{:?}", squares); // 5 строк. Цикл. Мутации. Грязно.
// Декларативный функциональный Rust let squares: Vec<i32> = (0..10) // Иммутабельно после завершения операции. .map(|x| x * x) // Не используем цикл — проходимся по каждому значению. .collect(); // Собираем результат в коллекцию. println!("{:?}", squares); // 4 строки, при желании сокращаются до двух. // Без мутаций. Без циклов. Чисто.
Декларативный подход экономит строки, убирая ненужные циклы делает код чище, избавляя от мутабельности делает код предсказуемее.
Проблема III: Некорректная или недостаточная обработка ошибок.
В императивном программировании все привыкли к исключениям. Бросил — поймал — забыл. В C++/Java вы просто пишете try { ... } catch { ... } и надеетесь, что никто не бросит std::exception или RuntimeException. В Python — try/except, и всё. Исключения стали ментальной моделью обработки ошибок.
Эта модель въелась в подкорку. Бросаем исключение — забываем. Потому что «где-то выше кто-то поймает». А если не поймает? Ну, упадёт. Подумаешь. В C++ упадёт, в Java упадёт, в Python упадёт. Зато писать быстро. Зато удобно. Зато не надо думать о типах ошибок.
И когда такие программисты приходят в Rust, они пытаются применить ту же модель. Но Rust — не Java. Исключений нет. Есть Result<T, E>. И тут начинается ломка. «Как это — нет исключений? А как же ошибки? А как же „поймаю где-нибудь наверху“?» Ответ Rust: «Ты будешь обрабатывать ошибку там, где она возникла. Или пробросишь её наверх явно. Но не надеяться, что кто-то другой о ней вспомнит».
Представьте, что вы купили старый тостер. Он работает. Но с вероятностью 1 к 10 вместо тостов он поджаривает вашу квартиру. Квартира сгорает дотла. Вы — без жилья. В императивном программировании никто вас не предупреждает. Вы вставляете хлеб, нажимаете кнопку и надеетесь. В функциональном программировании на коробке с тостером написано жирными буквами: «Result<Toast, Fire>». Это — не мелкий шрифт. Это знак внимания размером с Америку. Вы не можете его не заметить. Вы обязаны решить: что делать, если тостер решит спалить квартиру?
// Неправильная обработка ошибок в Rust old_toaster().unwrap(); // «Заткнись, компилятор! Всё будет окей!» old_toaster()?; // Мне просто лень. Да, это безопаснее, чем unwrap() // но всё же лучше делать другую логическую ветвь вместо такой обработки.
unwrap() — это не «я знаю, что здесь ошибки быть не может». Это «я надеюсь», а надежда — плохая стратегия. В продакшне надежда превращается в панику. А паника — в падение в 3 часа ночи и экстренный дебаг с плохим сном и самобичеванием в подарок.
? — это удобно. Но это — не обработка ошибки, а проброс её наверх. Это сигнал — «пусть эту ошибку разруливает тот, кто вызвал эту функцию». И она идёт выше и выше, пока не доползает до main(). А там она превращается в панику.
Решение: ОБРАБОТКА ошибок, а не их игнорирование.
// Правильная обработка ошибок в Rust let toast = old_toaster().map_err(|e| { eprintln!("Тревога: {}!!! Вызывай пожарных, Валера!", e); call_firefighters(); std::process::exit(255); // Эвакуируемся! }); let toast = match old_toaster() { Ok(t) => t, // Ням-ням! Err(e) => { eprintln!("Тревога: {}!!! Вызывай пожарных, Валера!", e); call_firefighters(); std::process::exit(255); // Эвакуируемся! }, }
Да, это дольше. Да, это больше строк. Да, надо думать. Но это честно: вы знаете, что будет, если тостер сломается. Вы знаете, что будет, если прод упадёт. Или не упадёт, если ошибка обработана. Можете спать спокойно.
Конечно, есть и другие способы: map_err, context, anyhow, thiserror. Но о них вы, я надеюсь, знаете. Этот проект — не учебник по Rust. Это спасение от нечитаемого императивного кода. Если вы хотите научиться писать на Rust функционально — вы по адресу. Если вы ещё не знакомы с языком — вы знаете, что делать: идти и читать The Book, и лишь потом возвращаться. Здесь вас научат писать правильно ;)
В обработке ошибок в Rust функциональный подход — это единственный правильный подход. Не игнорируйте ошибки. Не ленитесь их обрабатывать. Никогда. Сначала покажется, что это усложняет код. Но это не «усложнение». Это честность. А цена нечестности — ваша бессонная ночь, гневный звонок от заказчика и чувство собственной глупости, когда вы смотрите на unwrap() в логе и думаете про себя: «Я же знал. Я же знал, что здесь может быть ошибка». Как громко вы будете материться в 3 часа ночи? Вопрос риторический. Достаточно громко, чтобы разбудить соседей. Не доводите до этого. Обрабатывайте ошибки.
Следующие проблемы — это уже не про Rust (там они решены на уровне языка). Это про другие императивные и объектно-ориентированные языки, которые до сих пор мучают разработчиков по всему миру, и которые исправило ФП.
Проблема IV: Неявная пустота.
Представьте, что вы купили айфон. Счастья полные штаны, телефон куплен, но открывая коробку, вы обнаруживаете там пустоту. Вы хотите выплеснуть весь гнев на барыгу, который продал вам пустую красивую коробку. Вы перестаёте верить в людей. Вы перестаёте доверять коробкам.
// JavaScript let x = iPhoneBox(); // нет уверенности, что эта функция не вернёт null console.log(x); // null? Айфон? Кто знает... // А если повезёт — там айфон. А если нет — undefined или null. // И вы узнаете об этом только когда попытаетесь позвонить маме.
В JavaScript, Java, Python, C# и других императивных языках любое значение может быть null. Вы не знаете, где он спрячется. Вы не знаете, когда он вылезет. Вы только надеетесь, что «здесь точно не null». Но надежда — плохая стратегия. Тони Хоар, создатель null, назвал своё творение «ошибкой на миллиард долларов», но он не может от неё отказаться — слишком много кода написано с использованием null.
Решение — явная пустота.
ФП по умолчанию решает эту проблему: значение может быть пустым только явно — с помощью Option<T>:
// Rust let x: IPhone = IPhone::new(); // Эта функция ТОЧНО вернёт структуру IPhone x.call(mom); // Вы точно не позвоните по воображаемому телефону let maybe_none: Vec<Candy> = match get_candies() { // Паттерн-матчинг Some(c) => c, // Собираем конфеты в коробку None => Vec::new(), // Просто создаём новую коробку };
Option<T> — сигнал: этого значения может не быть. Если возвращаемый тип функции не обёрнут в Option — вы не можете получить пустоту, даже если очень захотите.
Проблема V: Классы.
В 80-х и 90-х классы казались революцией. Наконец-то можно было собрать данные и методы в одном месте! Это было свежо. Это было модно. Это обещало переиспользование, иерархии, полиморфизм. На практике же получился культ, который породил больше проблем, чем решил.
Проблема классов № 1: захламление кода.
Представьте себе коробку со старой техникой. Там есть всё — ваша «Денди», старые айфоны, может быть даже ваш спиннер из 2017 там затерялся, и вот это вот всё несут в себе классы.
В C++ эта проблема стоит наиболее остро, поскольку логика в классах делится на два файла: заголовок и сам код.
// myClass.h class MyClass { public: int myCoolVar; MyClass(int number); // Конструктор ~MyClass(); // Деструктор void endMyCruelLifePlease(); // Метод }
В данном файле есть только прототипы. Они все объявлены, но в них нет логики. Мы просто объявили, что конструктор, деструктор и метод существуют.
// myClass.cpp — здесь начинается ад #include "myClass.h" MyClass::MyClass(int number) { // ПОЖАЛУЙСТА myCoolVar = 10; } MyClass::~MyClass() { // ХВАТИТ // ... } MyClass::endMyCruelLifePlease() { // ОСТАНОВИТЕСЬ ++myCoolVar; }
Код на C++ физически и психологически невыносимо больно читать. И это один класс. А в реальном проекте их сотни. Тысячи. И каждый раз вы прыгаете между .h и .cpp, пытаясь понять, что же здесь происходит. Вы не программист — вы археолог, а иногда даже сапёр.
C++ не может отказаться от старой архитектуры, так как ему надо быть совместимым с C. C++ — помойка, в которой за 40 лет накопилось много всего, а начиная с C++20/23 там появилось и функциональное программирование, но оно выглядит ужасно грязно:
// C++20: функциональщина через переборы (выглядит как издевательство) #include <ranges> #include <vector> #include <iostream> int main() { std::vector<int> nums = {1, 2, 3, 4, 5}; auto result = nums | std::views::filter([](int x) { return x % 2 == 0; }) | std::views::transform([](int x) { return x * x; }); for (int x : result) { // И снова цикл. Нельзя просто взять и собрать. std::cout << x << " "; } // А в Rust мы бы просто написали: // let squares: Vec<i32> = (0..10).map(|x| x * x).collect(); // И никакого цикла. }
Решение: Разделить данные и логику, объединить всё в один файл.
В современных языках всё в одном файле, и у ФП нет классов: логика — отдельно, данные — отдельно.
// Rust: всё в одном месте. Никаких .h/.cpp. struct MyStruct { my_cool_var: i32, // Данные — в АТД! } impl MyStruct { // Логика — отдельно! fn new() -> Self { Self { my_cool_var: 10 } } fn my_method(&self, to_add: i32) -> i32 { self.my_cool_var + to_add } }
Код читается. Нет заголовочных файлов. Не нужно прыгать между файлами, не нужно гадать, где что объявлено.
Проблема классов № 2: Иерархия и наследование
В ООП класс можно унаследовать от другого класса:
// C++ class Animal { public: void speak(); } class Dog : Animal { // наследуем от Animal void speak() { // переопределяем std::cout << "Гав!" << std::endl; } } class Bird : Animal { // наследуем тоже от Animal void speak() { // переопределяем std::cout << "Чирик!" << std::endl; } } class DogBird : Dog, Bird { DogBird() { speak(); // вот тут-то и начинается веселье // Какой speak? Dog::speak? Bird::speak? // G++/MSVC/CLang++: Я не знаю, ты уж сам решай } }
Методы переплетаются. Мы наследуем два класса с одним и тем же методом. Начинается неопределённое поведение. Чей метод вызовется? В Python эта проблема решена частично, но это как синяя изолента на трещине на стене:
# Python class DogBird(Dog, Bird): def __init__(self): super().__init__() self.speak() # Кто первее, тот и прав! Вызовется метод от Dog. # Но если нам нужен от Bird, мы явно должны вызывать метод от Bird
Данная проблема называется «проблемой ромба», или «ромбовидное наследование», или «ромб смерти». Ромбом его назвали из-за формы иерархии:
A / \ B C \ / D
И это — не баг. Это — архитектура, обещавшая переиспользование, но в итоге заставившая программистов:
Трясти дерево наследования, в надежде, что несчастному программисту не упадёт яблоко на голову, как на Ньютона.
Аккуратно менять родительские классы, чтобы не поломать дочерние.
Страдать от «ромба смерти».
Решение: Избавиться от наследования, заменив его на композицию и трейты
В ФП нет классов и наследования. Вместо них есть:
// Композиция типов в АТД! struct Park { dog: Animal, bird: Animal, } // Трейты и их реализация! trait Animal { fn speak(&self); } impl Animal for Dog { fn speak(&self) { println!("Гав!"); } } impl Animal for Bird { fn speak(&self) { println!("Чирик!"); } }
Проблема классов № 3: Инкапсуляция, а точнее её смысл.
В ООП можно любую переменную сделать приватной и подключить к ней геттер/сеттер:
// C++ class MyClass { private: int myCoolVar; public: int getVar() { // Геттер return myCoolVar; } void setVar(int num) { // Сеттер myCoolVar = num; } }
Но у меня складывается библейско-философский вопрос: а зачем вообще её делать тогда приватной, если любой может её изменить? Это всё равно, что повесить на комнату табличку «не входить» — но оставить дверь открытой и повесить рядом ключ с надписью «если хочется, можете войти». Инкапсуляция? Какая ж это инкапсуляция, это бюрократия!
Решение: Не прописывать геттеры/сеттеры, делать значения публичными, и иммутабельно работать с ними.
// Rust struct MyStruct { pub my_cool_val: i32, } fn main() { let my_struct = MyStruct { my_cool_val: 10, }; let processed = my_struct.my_cool_val + 10; println!("{}", processed); }
Такой подход даже не требует реализации методов. Код короче и читаемее. Если нужно защитить значение — не обязательно делать его приватным: можно сделать его иммутабельным. Иммутабельность — настоящая инкапсуляция.
Проблема классов № 4: Слабый полиморфизм.
Полиморфизм — то, за что некоторые любят ООП. Но в ФП полиморфизм тоже есть! И он даже круче, чем в ООП.
В ООП вы создаёте хрупкую структуру с наследованием, виртуальными таблицами с оверхедом, и чтобы расширить полиморфизмы, вам придётся менять всю иерархию.
// ООП-полиморфизм в C++ class Shape { public: virtual double area() = 0; }; class Circle : public Shape { double r; public: Circle(double radius) : r(radius) {} double area() override { return 3.14 * r * r; } }; class Square : public Shape { double s; public: Square(double side) : s(side) {} double area() override { return s * s; } }; double totalArea(vector<Shape*>& shapes) { double sum = 0; for (auto s : shapes) sum += s->area(); return sum; } // 26 строк.
Решение: Функциональный полиморфизм с более простыми правилами.
В ФП вместо жёсткой иерархии предлагается свободная горизонтальная иерархия с трейтами, а также есть дженерики — можно реализовать один и тот же метод, который может принять любой тип, реализующий нужный трейт.
Также, чтобы изменить полиморфизм, вы не меняете всю иерархию в страхе что-либо сломать. Вы меняете трейт, либо делаете новый.
// Функциональный полиморфизм в Rust trait Shape { fn area(&self) -> f64; } struct Circle { r: f64 } impl Shape for Circle { fn area(&self) -> f64 { 3.14 * self.r * self.r } } struct Square { s: f64 } impl Shape for Square { fn area(&self) -> f64 { self.s * self.s } } fn total_area<T: Shape>(shapes: &[T]) -> f64 { shapes.iter().map(|s| s.area()).sum() } // 17 строк. Чистый код. Дженерики. // На 9 строк меньше. Без наследования. // Без виртуальных таблиц. Без риска сломать иерархию.
Кстати, реализовать функциональный полиморфизм можно даже для примитивных типов!
// Полиморфизм для примитивных типов в Rust trait Speak { fn speak(&self); } // Полиморфная функция (дженерик) fn make_sound<T: Speak>(obj: T) { obj.speak(); } impl Speak for i32 { fn speak(&self) { println!("{} говорит: я — число!", self); } } make_sound(67); // 67 говорит: я — число!
То же самое, кстати, и с другими языками функционального программирования. Не один Rust так крут:
-- Полиморфизм в Haskell class Speak a where -- это не ООП-класс, это вроде трейта в Rust speak :: a -> String instance Speak Dog where speak _ = "Гав!" instance Speak Cat where speak _ = "Мяу!" makeSound :: Speak a => a -> IO () makeSound animal = putStrLn (speak animal)
Такой полиморфизм не нагружает систему в рантайме: вместо этого он компилируется в мономорфные структуры, и за счёт этого полиморфизм не стоит вам производительности в функциональном программировании!
Проблема VI: Типизация.
В 90-х программисты устали от типов. В C++ приходилось писать std::vector<int>::iterator и молиться, чтобы компилятор понял. И появились языки, которые обещали свободу: Python, Ruby, JavaScript, PHP. Динамическая типизация казалась освобождением. Не надо думать о типах. Пиши — и работает.
И это сработало. Прототипы стали пилиться за часы, а не за дни. Казалось, что статическая типизация умерла. Что динамика победила. Что будущее — за языками, где «всё работает, пока не сломалось».
Но свобода без ответственности быстро превратилась в хаос и безнаказанность.
// JavaScript let user_id = 67 console.log("Hello, user #" + user_id) // Вау, как круто! // Число само преобразуется в строку! Не надо думать!
Интерпретатор превратился в терпилу, которого можно бить, и он вам ничего не сделает:
// JavaScript let x = Number("ПРИВЕТ"); console.log(x); // NaN
Он просто схавает то, что вы ему дали, и выдаст, что может. Он не скажет «вы ошиблись», «передана строка, ожидалось число». Он просто выдаст NaN (Not a Number) и пойдёт дальше. А потом гадай, почему в базе данных появилось NaN, и почему все расчёты пошли вразнос, и почему прод упал в три часа ночи.
// JavaScript console.log("2" + 2); // Это конкатенация console.log("2" - 2); // Это вычитание // Угадайте, к какому типу что будет приведено.
Один оператор конкатенирует строки, потому что он перегруженный — он может и конкатенировать строки, и складывать числа. Другой — только вычитает числа. Как JavaScript решает, что делать? Он гадает. У него есть правила, но они настолько запутанные, что даже опытные разработчики не всегда помнят, что будет в [] + [](пустая строка) или {} + [] (0). Это не программирование. Это угадайка. Если вы не можете быть уверены в значении — ваш код ненадёжен.
С динамической типизацией и неявными преобразованиями программирование стало проще. Но дебаг — сложнее. Скорость — иллюзия. Без управления типами вы больше потратите времени на дебаг, чем на программирование.
# Python def get_user_name(user): # А кто такой user? Словарь? Класс? return user.name # А какой тип? Есть ли вообще поле name у user?
В Python это называется «утиная типизация»: если это ходит как утка и крякает как утка — это утка. Но что, если это не утка? Что, если у вас в продакшн прилетел гусь без поля name? Python скажет AttributeError. Когда? В самый неподходящий момент. Например, в пятницу вечером в продакшне.
Причём есть языки как с динамической типизацией, как вышеперечисленные, так и со слабой, вроде C:
// C int x = 67; printf("%s\n", x); // 'C', потому что 67-й символ в ASCII — 'C'. // Компилятор может предупредить. А может и нет. // В любом случае, он скомпилирует. // А в продакшене вместо числа выведется символ. // Или краш. Или всё вместе.
C — это язык, где типы — это рекомендация. Компилятор может предупредить, но скомпилирует. А в продакшене вместо числа выведется символ. Или краш. Или всё вместе.
Решение: Строгие типы.
Строгие типы в ФП заставляют потратить чуть больше времени на программирование, чтобы не тратить всю ночь на дебаг.
// Rust: строгая типизация fn get_user_name(user: User) -> Result<String, Error> { // Тип говорит всё. user — это User. // Сигнатура функции — контракт. // Вернёт либо String, либо ошибку. Ok(user.name) } // let x = "2" + 2; // Так нельзя. let x = format!("2{}", 2); // А вот так можно. Сразу понятно, что нужно.
Ни один тип не преобразуется в другой неявно. Вы не можете сложить строку с числом. Вам нужны format!, to_string, parse, либо другие функции для преобразования типа. И так не только в Rust — это суть всего функционального программирования. Если вам нужен другой тип — преобразуйте его явно:
-- Haskell main :: IO () main = putStrLn $ show 67 -- Явно преобразуем Int в String c помощью show
Результат говорит сам за себя: ФП выбирают там, где ошибки — это не «ой, упало», а катастрофа. Финансы. Медицина. Авиация. Космос. Потому что у языков функционального программирования самый высокий аптайм среди всех. Они ближе всего к 100%. И это не потому, что разработчики — гении. Это потому, что ФП по своей природе не даёт делать глупые ошибки: типы проверяются, пустоты нет, мутация — под контролем. Компилятор — не враг. Он — страхующий трос.
Проблема VII: Исключения.
Раньше, когда исключений ещё не было, программа могла повести себя неправильно, натворить дел, или упасть с ошибкой сегментации за доступ к чужой памяти:
// C int* ptr = 67; *ptr = 256; // И камнем вниз...
Это было больно. Программа просто умирала. Без возможности отследить, где именно.
После, появились исключения. Идея казалась гениальной. Программа не умирала: она бросала исключения. Можно его поймать, обработать и даже продолжить работу.
// C++ try { riskyOp(); } catch (const std::exception& e) { std::cerr << "Ошибка: " << e.what() << std::endl; // Ошибка обработана, продолжаем работу. // Вроде бы продолжаем. Но что, если riskyOp() // бросила не std::exception, а int? // Или const char*? Или вообще ничего не бросила, а просто упала? // Вы не знаете. Вы надеетесь. }
Но проблема исключений заключается в том, что они нечестные.
Проблема исключений № 1: Вы не знаете, что именно может упасть.
// C++ void readFile(const std::string& path) { std::ifstream file(path); if (!file.is_open()) { throw std::runtime_error("Не удалось открыть файл"); } // ... }
Вы читаете код. Вы видите throw. Вроде бы, всё под контролем: вы знаете, какая функция бросает исключение. А что насчёт std::ifstream? std::string? Оператора <<? А конструкторы? А деструкторы?
Вы ничего не знаете. В C++ любая функция бросит исключение, если она не помечена явно как noexcept, и никто вас не предупредит: ни компилятор, ни ваш сосед, НИКТО. Если что-то пошло не так — читайте документацию. Или молитесь.
В Java ровно то же самое. ООП построено на исключениях.
// Java void riskyOp() throws IOException { // ... }
Здесь хотя бы честно: функция может выкинуть IOException, может вылезти ошибка ввода-вывода. Но что насчёт всеми любимого NPE? Его можно не объявлять. Он вас будет преследовать везде и всегда — как налоги, как капитализм, как судьба, ну или как озабоченный сталкер. Это — неубиваемая тварь, поскольку абсолютно всё в Java может быть null. И вы не знаете, когда он вылезет: сейчас, или когда он пришлёт вам привет в 3 часа ночи из прода.
Проблема исключений №2: А за собой убирать кто будет?
Вы открыли файл, аллоцировали память, взяли блокировку, и поймали маслину исключение.
// C++ void dangerous() { int* ptr = new int[1000]; std::ifstream file("data.txt"); // ... какая-то операция, которая бросает исключение delete[] ptr; // Сюда никогда не дойдём // file закроется в деструкторе? Повезёт, если да. }
В C++ есть RAII (Resource Acquisition Is Initialization), который помогает: деструкторы вызываются при раскрутке стека (выходе из области видимости). Но:
Это работает только для умных указателей, не для сырых.
Это работает, если вы правильно и грамотно написали деструкторы.
Это работает, если вы не забыли закрыть ресурс в деструкторе.
В Java, Python, JS, C# — сборщик мусора. Он соберёт, когда-нибудь. Может быть, через минуту. Может быть, через час. Может быть, когда файловых дескрипторов не останется.
Исключения не гарантируют, что ресурсы будут освобождены: это оставляют на вашу совесть.
В функциональных языках, вроде Haskell, Scala и F# сборщик мусора, кстати, тоже есть, и поэтому я и веду этот блог; поэтому я выбрал именно Rust и именно ФП: функциональщина — не враг скорости.
Проблема исключений №3: Исключения — это goto на стероидах.
Помните goto? Это он сейчас:
// C++ void a() { b(); } void b() { c(); } void c() { d(); } void d() { throw std::runtime_error("Опаньки..."); } int main() { try { a(); } catch (const std::exception& e) { // Поймали. А кто бросил? Где? Почему? // Нужно смотреть стектрейс. } }
Это как goto, только с неявными переходами. Вы не видите поток управления. Вы не знаете, где исключение будет обработано. Вы только знаете, что оно может быть. Или не быть. Или быть где-то ещё.
Проблема исключений №4: Исключения — это дорого.
Исключения в C++ и Java не бесплатны. Они:
Замедляют код, даже если исключение не брошено (нужно готовить таблицы для раскрутки стека).
Требуют выделения памяти для создания объекта исключения.
Требуют обхода стека в поисках обработчика.
Совершенно не подходят для высоконагруженных систем (привет, MISRA!).
Решение: Избавиться от исключений. Принять Option и Result.
В ФП нет исключений. Вообще. Вместо этого в Rust есть два стула:
Option<T>— когда значения может не быть.Result<T, E>— когда можете получить либо значение, либо ошибку.
Кстати, если среди читателей есть функциональщики на опыте в других языках, я сам заметил, что эти две штуки — на самом деле монады ;)
// Rust fn get_user(id: u64) -> Result<User, Error> { // Может вернуть Ok(user) или Err(error) } fn main() { match get_user(42) { Ok(user) => println!("Юзер: {}", user.name), Err(e) => eprintln!("Ошибка: {}", e), } }
// Rust fn process() -> Result<(), Error> { let user = get_user(42)?; // Если ошибка — выходим let data = user.load_data()?; data.save()?; Ok(()) }
Что это нам даёт?
Честность. Возвращаемый тип функции говорит сам за себя.
Result<User, Error>— значит, может быть успех или ошибка.Option<User>— значит, может быть пользователь или пустота.User— значит, пользователь будет точно. Не может бытьnull. Не может быть исключения. Не может быть неожиданности. Всё предсказуемо и понятно.Композиция. Оператор
?из примера выше — это как «попробуй выполнить, если ошибка — выйди». Чисто. Явно. Без скрытых переходов и побочных эффектов.Безопасность ресурсов. RAII в Rust работает всегда. Когда переменная выходит из области видимости, её ресурсы освобождаются. Исключений нет. Нет «а вдруг не вызовется деструктор». Всё предсказуемо и явно.
Производительность. В некоторых бенчмарках Rust обгоняет C++ по скорости, так как
OptionиResult— это самые обычные типы, а если быть точнее — АТД (мы до этого ещё дойдём). Нет скрытых таблиц. Нет обхода стека. Нет выделения памяти. Всё на стеке, всё быстро.
Всё это — заслуга не только Rust, но и ФП в целом. Это кстати ещё одна причина высокого аптайма функциональных языков.
Мы разобрали, чем плох старый, императивный ООП-подход. Но что особенного может дать ФП, кроме решения вышеперечисленных проблем? Три буквы:
АТД.
Или же Алгебраические Типы Данных.
АТД — это не просто типы. Это алгебра, где данные подчиняются законам.
Вы не можете создать невалидное состояние. Не можете забыть обработать вариант. Не можете перепутать поля. Компилятор проверяет всё, что можно проверить. А то, что нельзя — вы просто не можете выразить на языке типов.
В этом и заключается мощь и крутизна функционального программирования: не в монадах, не в чистых функциях, а в том, что типы не врут.
Почему Rust — один из особенных языков с АТД? Почему этого не могут предложить другие языки? Вот вам примеры ниже.
В Python и JS нет АТД вообще:
# Python # enum? struct? Что это? @dataclass # эмуляция struct... class Square: side: int
// JavaScript // interface? enum? Их добавят только в TypeScript.
В C АТД либо слабые, либо синтаксический сахар над int:
// C typedef struct { double x, double y } Point; // ну что это такое? Это разве АТД? // Просто слабенькая группа данных. typedef enum { IDLE, ONLINE, IN_GAME, OFFLINE } Player; // это вообще синтаксический сахар над int! ONLINE == 1...
В TS они просто слабые:
// TypeScript interface ButtonProps { type: "default" | "secondary" | "disabled"; onClick: () => void; } // Ну это хоть как-то можно притянуть на АТД... enum User { Online, Offline, Idle } // А это вообще не АТД. Это просто enum. // В конечном итоге, это просто надстройки над JS. В рантайме они исчезнут.
Функциональное программирование подарило миру АТД, в том числе и Rust.
Это — суммируемый тип, также известен как «ИЛИ». Значение может быть одним из вариантов.
// Rust enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Triangle { base: f64, height: f64 }, } // С композицией типов enum Option<T> { Some(T), None, } // Да, Option<T> — не только монада, но и АТД!
А вот это — производимый тип, также известен как «И». Значение содержит все поля одновременно.
// Rust struct Point { x: f64, y: f64, }
Композиция — это когда вы строите сложные типы из простых. Тоже особая фишка АТД.
// Rust struct Party { friends: Vec<Friend>, food: Vec<Pizza>, music: Music::Dubstep, }
Почему это круто?
Типы отражают свою предметную область. Если в вашей программе есть
Shape, который может бытьCircle,RectangleилиTriangle, то типShapeговорит об этом. Не документация. Не комментарии. Тип.Компилятор проверит полноту. Когда вы пишете
matchпоShape, компилятор проверит, что вы обработаливсеварианты. ЗабылиTriangle? Не скомпилируется. Это не «документация». Это контракт.Нет «недопустимых состояний». В ООП можно создать объект в некорректном состоянии. В ФП — нельзя. Тип не позволит.
enum User { Anonymous, Registered { id: u64, name: String }, } // Нет состояния, когда пользователь зарегистрирован, но без id. // Нет состояния, когда пользователь анонимный, но с id. // Тип не врёт.
Композиция вместо наследования даёт контроль вместо хрупких иерархий. Ваша структура не является чем-то — она содержит что-то. Ваша коробка с айфоном не является потомком коробки — она содержит в себе айфон.
struct MyBox<T> { content: Option<T>, } struct IPhone { os: IOS, charge: i8, on: bool, apps: Vec<App>, } enum App { Slack, Minecraft, Discord, Telegram, Element, YouTube, } fn main() { let phone = IPhone { os: IOS, charge: 100, on: true, apps: vec![Slack, Minecraft, Telegram, Element, YouTube], }; let my_box = MyBox { content: Some(phone) }; }
Что дают нам АТД в ФП и в Rust?
Option<T>— это суммаSome(T)илиNone. Нетnull. Нет NPE. Только честное «может быть, а может и нет». А ещё это монада, но это для тех, кто шарит ;)Result<T, E>— это суммаOk(T)илиErr(E). Нет исключений. Нет «да ладно тебе, что может пойти не так?». Тип говорит: «Здесь могла затаиться ошибка. Пристегни ремни и готовься к жёсткой посадке в случае чего».Компилятор как соавтор: вы не можете забыть обработать вариант в суммируемых типах. Не можете создать некорректное состояние. Не можете обратиться к полю, которого нет.
// Rust: паттерн-матчинг суммируемого типа match user { User::Anonymous => println!("Кто ты?"), User::Registered { id, name } => println!("Привет, {}!", name), }
Почему это важно для ФП?
Функциональное программирование — не «задротская темка», не только монады и функторы. Это — инструмент жёсткого контроля происходящего в коде. Rust даёт больший контроль по умолчанию, но в функциональном стиле — максимум контроля. Это про типы, которые не врут. Про данные, которые нельзя изуродовать и покалечить. Про компилятор, который двести тысяч раз проверит вашу логику, и скажет «да, это не упадёт в проде».
Где ФП вам не пригодится?
Кратко, без нытья и погружения в нишевые глубины: ФП — не для системного программирования. Не забывайте, компьютер — тупой, но послушный и очень быстрый раб. Все эти map, filter, fold, Option, Result — они живут в стандартной библиотеке. А в низкоуровневом коде этой библиотеки нет.
Rust без std — это другой Rust. Там нет Vec<T>. Нет Box. Нет итераторов. Там голые массивы, сырые указатели, ручное управление памятью и надежда, что вы не ошибётесь на байт.
На чипе с 2 КБ RAM и 8 КБ флеш-памяти вы не будете писать numbers.iter().map(|x| x * 2).collect(). Вы будете писать циклы. Вы будете мутировать переменные. Вы будете считать каждый байт и каждый такт.
TL;DR: Функциональное программирование — высокоуровневое. Это аксиома. Я мог бы не расписывать. Но сказал, чтобы вы не пытались забивать гвозди микроскопом.
Итог
Функциональное программирование не ново. Оно ждало своего часа десятилетиями. И только сейчас, когда железо перестало быть узким местом, а компиляторы научились оптимизировать абстракции, программисты начали понимать: самый надёжный код — это код, написанный в функциональном стиле.
Такой код не падает. Такой код читается. Такой код можно рефакторить без страха. Такой код позволяет спать ночью.
Почему Rust?
Rust — это функциональный язык в системной обёртке. Он взял от ФП всё самое важное: АТД, паттерн-матчинг, монады в виде Option и Result, функциональные итераторы. Но добавил то, чего ФП всегда не хватало: производительность.
Благодаря отсутствию сборщика мусора, строгой системе владения, чётким временам жизни и абстракциям с нулевой стоимостью, Rust стал самым быстрым языком функционального программирования. F#, Haskell, Lisp, Scala — все они уступают Rust в скорости и потреблении ресурсов.
Раньше ФП ругали за медлительность. «Это не для серьёзных задач», «это только для прототипов». Теперь у нас есть Rust. И последнего недостатка функционального программирования больше не существует.
Функциональный Rust — это не просто «ещё один язык». Это мост между миром высокой абстракции и миром голого железа. Это ФП, которое работает. Быстро. Безопасно. Предсказуемо.
Теперь выбор за вами. Продолжать писать императивный код и гадать, почему прод упал в 3 часа ночи. Или открыть редактор и попробовать функциональный Rust. Без mut. Без циклов. Без unwrap(). С чистым кодом. И со спокойным сном.
Комментарии (48)

amcured
03.04.2026 06:52F#, Haskell, Lisp, Scala — все они уступают Rust в скорости и потреблении ресурсов.
Citation needed. Да и в целом, «язык А быстрее языка Б, вот и пластмассовые бенчмарки подоспели» — это либо глупость, либо вредительство. Если вагон задач, в которых любой из вышеперечисленных языков уделает раст, как бык — овцу.
Раньше ФП ругали за медлительность. «Это не для серьёзных задач», «это только для прототипов».
Citation needed. Да и в целом, хаскель ругают за одно, а скалу — за совсем другое.
[…] попробовать функциональный Rust. Без
mut.Совет прям от Григория Остера. А как насчет стейта в асинхронных тредах? В базу сохранять?
Не, я понимаю, что ваш подход прекрасно работает на примерах в три строки из туториала. Но вы тут заикались про продакшн, а там, знаете, бывает хайлоад, большая конкурентность, долгоживущие треды, и всякая другая непотребщина.
Не нужно тащить в джаву лямбды, в джаваскрипит классы, в раст — иммутабельность, пожалуйста. Возьмите подходящий вашим препочтениям язык (функциональных иммутабельных языков — полно́) — и пишите на нем. А в расте даже акторная модель через жопу сбоку прикручена.

v_0ver
03.04.2026 06:52Если вагон задач, в которых любой из вышеперечисленных языков уделает раст, как бык — овцу.
Citation needed.

Boneyan
03.04.2026 06:52Проблема II: Императивность
Вот тут не согласен с предложенным решением. Имхо плодить пятистрочные функции по типу bring_beer это вредный подход. Это выглядит красиво, не несёт никакой практической пользы, и в будущем усложняет отладку и понимание кода в целом. Я ещё могу понять, если это делается в нескольких местах сразу (но в таком случай любой, кто слышал про DRY выделит это в функцию, и про такое в статье можно и не писать). А делать пятистрочные функции, которые вызываются ровно в одном месте - это просто портить опыт чтения кода сверху-вниз, когда приходится прыгать куда-то там, где объявлена эта маленькая функция.
Проблема III: Некорректная или недостаточная обработка ошибок.
Здесь я бы упомянул, что неожиданно может оказаться, что вызываемая функция паникует, и это будет такой же тостер, сжигающий дом. Но nopanic это отдельная банка червей, как говорится.
Проблема исключений № 1: Вы не знаете, что именно может упасть.
В расте есть такая же проблема, хоть и в меньшей степени. Вызываемая функция может неожиданно запаниковать, как я уже выше писал.
Немного ошарашивает ваш религиозный фанатизм в отношении функционального программирования и поливание помоями императивного. Вам бы попросить чажпт (или с чьей помощью вы пишете) подуспокоится. Но может мне просто такой стиль не заходит :)
И немного раздражает вот этот подход с аналогиями, где каждый пункт начинается с “представьте…”. Ей богу, вы не для детсадовцев пишете, а для инженеров, тут не надо на грушах и яблоках объяснять.

v_0ver
03.04.2026 06:52В расте есть такая же проблема, хоть и в меньшей степени. Вызываемая функция может неожиданно запаниковать, как я уже выше писал.
Это не совсем так,
#[no_panic]может в рамках крейта доказывать, что указанная функции не паникует. А при сборке сLTO, и с учётом других крейтов (транзитивно).
Boneyan
03.04.2026 06:52Ну так это сторонняя библиотека и вроде как у неё есть какие-то заковырки? В любом случае не часть стандартной поставки языка

v_0ver
03.04.2026 06:52Да, это сторонняя библиотека, но там нет никакой магии и заковырок, всего 209 строчек кода. Работает она как раз за счёт того, что в Rust достаточно легко найти все места где может паниковать программа
panic="unwind".

apevzner
03.04.2026 06:52Ну и ничего из перечисленного не является какой-то специфической особенностью именно вот функционального стиля.
Обработка ошибок путём явного возврата их по значению в Rust-е вообще из Go, а в Go из Alef, его прямого предшественника от тег же авторов. А Alef - это диалект Си. Вот уж никогда никто не назвал бы его функциональным языком.
А что до неприменимости ФП для системного программирования, и тут бы я тоже поспорил. Системное программирование - это отнюдь не только про перекладывание байтов из регистра в регистр. Более того, критический, в плане производительности, путь - это процентов 10 общего кода. А остальное - код логически сложный, но без особых притязаний в плане производительности. И писать его в стиле “выжимаем каждый такт из каждого байта” нет никакой необходимости. Скорее, его надо ментально упрощать, пусть даже и за счёт некоторых потерь в скорострельности, потому, что логические ошибки в этом коде слишком дорого обходятся.

kmatveev
03.04.2026 06:52Похая нейростатья, с дурацкими примерами.
Функциональный стиль не является декларативным ни в коем разе, и все эти фразочки "вы описываете, ЧТО вы хотите, а не КАК это сделать" не объясняют ничего. Функциональный стиль является функциональным, и демонстрировать его отличие от не-функционального надо так:
Сейчас будет не-функциональный (процедурный)
int a; int b; int result; void calculate() { sum = (2*a + 3*b)/7 - 5; } void main() { a = 5; b = 8; calculate(); print("Result is:" + result); }А сейчас - функциональный:
int calculate(int a, int b) { return (2*a + 3*b)/7 - 5; } void main() { int a = 5; int b = 8; int result = calculate(a, b); print("Result is:" + result); }Обе эти программы выдают правильный результат. С точки зрения CPU процесс вычисления в обоих случаях будет практически одинаковым (забудем про нюансы передачи параметров, считаем, что код заинлайнится). Принципиальна разница с точки зрения разработчика в том, как происходит shunting данных. В случае с функцией параметры в неё пропихиваются вызывающей стороной, у вызывающего нет никакой возможности не передать параметр, у него не получится забыть это сделать. А у функции нет необходимости куда-то идти за значениями, они в аргументах. Также у вызывающего функцию нет необходимости куда-то идти за результатом вызова, он не доступен никак иначе, кроме как значение, получаемое в месте вызова. Иммутабельность - это следствие.

orenty7
03.04.2026 06:52Статья, к сожалению, имеет мало общего с реальным ФП и больше похожа на агитку.
// Императивный Rust let mut squares = Vec::new(); // Мутабельно. for num in 0..10 { // Водим программу за ручку. squares.push(num * num); // Мутируем. } println!("{:?}", squares); // 5 строк. Цикл. Мутации. Грязно.В самих по себе мутациях нет ничего плохого. Проблемы начинаются тогда, когда они становятся нелокальными. В вашем примере
squaresэто новый вектор, в его мутациях нет ничего плохого, так как к нему не имеют доступа другие части программы. Более того, я спокойно могу перенести этот код в хаскель, который достаточно строго следит за мутациями:-- Иммутабельная переменная squares :: [Integer] squares = runST $ do -- Начало мутабельного блока squaresRef <- newSTRef [] -- Мутабельная переменная forM [0..9] $ \i -> do modifySTRef squaresRef (\old -> old ++ [i^2]) result <- readSTRef squaresRef return resultФП по умолчанию решает эту проблему: значение может быть пустым только явно — с помощью
Option<T>Option<T>— сигнал: этого значения может не быть. Если возвращаемый тип функции не обёрнут вOption— вы не можете получить пустоту, даже если очень захотите.foo :: Integer foo = undefinedghci> foo *** Exception: Prelude.undefined CallStack (from HasCallStack): undefined, called at /home/orenty7/projects/haskell/test/app/Main.hs:21:7 in main:MainТакой полиморфизм не нагружает систему в рантайме: вместо этого он компилируется в мономорфные структуры, и за счёт этого полиморфизм не стоит вам производительности в функциональном программировании!
Открываем core (промежуточный язык, в который компилируется хаскель):
makeSound1 = \ (@a) ($dSpeak :: Speak a) (animal :: a) (eta :: State# RealWorld) -> hPutStr2 stdout (($dSpeak `cast` <Co:2> :: Speak a ~R# (a -> String)) animal) True etaАргумент
$dSpeakэто указатель на структуруSpeak a, где лежит функцияspeak. Ничего не мономорфизировалось, производительность падает так же, как в ООП языках. И как в расте, при использованииdyn TraitНи один тип не преобразуется в другой неявно. Вы не можете сложить строку с числом. Вам нужны
format!,to_string,parse, либо другие функции для преобразования типа. И так не только в Rust — это суть всего функционального программирования. Если вам нужен другой тип — преобразуйте его явно:-- Haskell main :: IO () main = putStrLn $ show 67 -- Явно преобразуем Int в String c помощью showТолько если скормить этот код GHC, он выдаст:
• Defaulting the type variable ‘a0’ to type ‘Integer’ in the following constraints (Show a0) arising from a use of ‘show’ at app/Main.hs:39:19-22 (Num a0) arising from the literal ‘67’ at app/Main.hs:39:24-25Благодаря полиморфизму
67может быть любым типом для которого определён инстансNum. Потому что под капотом там что-то типаfromInteger (67 :: Integer), и результат может быть любым. В том числе строкой, если для неё есть инстансNum:instance Num String where fromInteger x = show x main :: IO () main = putStrLn 67В Java, Python, JS, C# — сборщик мусора. Он соберёт, когда-нибудь. Может быть, через минуту. Может быть, через час. Может быть, когда файловых дескрипторов не останется.
В питоне есть context manager, в JS try-finally. Java и C# я не пользовался, но поисковик выдаёт, что в Java есть
Closable,AutoClosableи try-with-resources, а в C# естьusingиIDisposableВ функциональных языках, вроде Haskell, Scala и F# сборщик мусора, кстати, тоже есть, и поэтому я и веду этот блог; поэтому я выбрал именно Rust и именно ФП: функциональщина — не враг скорости.
В хаскеле для этого есть
bracket, или, если вы достаточно смелый, чтобы тащить это в кодобазу –ContTВ ФП нет исключений. Вообще.
Их в хаскеле штуки четыре разных есть :
Нечёткие исключения – могут возникать в чистом коде
Чёткие исключения – бросаются в IO
Асинхронные исключения – прилетают из другого потока
MonadFail– специфичные для конкретной монады исключения:NothingдляMaybe,LeftдляEither, чёткие исключения для IO, и так далее
Композиция. Оператор
?из примера выше — это как «попробуй выполнить, если ошибка — выйди». Чисто. Явно. Без скрытых переходов и побочных эффектов.Одна только проблема:
Result<A, E1>иResult<B, E2>нормально не композируются. Дальше идут увлекательнейшие приседания в попытках объединить эти два типа ошибкок.В TS они просто слабые
Вы просто не умеете их готовить:
type Option<T> = { marker: 'none' } | { marker: 'some', value: T }// В конечном итоге, это просто надстройки над JS. В рантайме они исчезнут.
Вы умудрились попасть в едва ли не единственную фичу тайпскрипта, которая доживает до рантайма: Enums are real objects that exist at runtime (source)

IUIUIUIUIUIUIUI
03.04.2026 06:52Исключительно из любви к формальности:
foo :: Integer foo = undefinedХаскелисты традиционно притворяются, что
undefined(и прочих нетотальностей) не существует, и вообще что Hask — настоящая категория.Олсо, если подходить формально, то
ФП по умолчанию решает эту проблему: значение может быть пустым только явно — с помощью
Option<T>имеет некоторый смысл даже в нетотальных языках при наличии
undefined,errorи прочих подобных: эти «пустые» значения невозможно наблюдать в чистой части программы в рамках стандартной метатеории [разных вершин] лямбда-куба, без компиляторной магии (ST/IO— компиляторная магия,seq— компиляторная магия, да и она тоже не меняет большую операционную семантику, потому что сходящиеся термы что сseq, что безseqимеют одну и ту же нормальную форму).Иными словами, ты в чистом коде не можешь написать
isBottom :: Int → Bool isBottom n = n == ?⊥?поэтому про наличие пустоты ты не узнаешь.
Аргумент $dSpeak это указатель на структуру Speak a, где лежит функция speak. Ничего не мономорфизировалось, производительность падает так же, как в ООП языках.
После инлайнинга и/или при наличии доступного анфолдинга в использующем модуле оно вполне может специализироваться, если компилятор решит, что это выгодно (я про это целый пост писал пару лет назад!)
И как в расте, при использовании dyn Trait
Для этого в хаскеле тоже надо явно обмазываться экзистенциальными типами, ну либо убить оптимизатор (что можно сделать в любом языке).

orenty7
03.04.2026 06:52имеет некоторый смысл даже в нетотальных языках при наличии
undefined,errorи прочих подобных: эти «пустые» значения невозможно наблюдать в чистой части программы…Прикольно, не задумывался об этом. Хотя следовало бы, учитывая, что в статье про imprecise exceptions вопрос наблюдаемых и ненаблюдаемых штук в чистом коде довольно подробно разбирался
без компиляторной магии (
ST/IO— компиляторная магия,seq— компиляторная магия…)Как с помощью
IOпронаблюдать понятно, а как можно с помощьюSTилиseq?После инлайнинга и/или при наличии доступного анфолдинга в использующем модуле оно вполне может специализироваться, если компилятор решит, что это выгодно
Так-то и в объекто-ориентированных языках вызовы девиртуализироваться могут, но полагаться на оптимизатор ни в ООП, ни в ФП, имхо, не стоит. Особенно как автор статьи, заявляющий, что вообще всё мономорфизируется.
я про это целый пост писал пару лет назад!
А не осталась, случайно, ссылка?

IUIUIUIUIUIUIUI
03.04.2026 06:52Как с помощью
IOпронаблюдать понятно, а как можно с помощьюSTилиseq?ST— это обобщённыйIO(в прямом смысле,IO ≅ ST RealWorld).seq— так, для полноты. Его расходящийся результат тоже ненаблюдаем в этом же смысле.Так-то и в объекто-ориентированных языках вызовы девиртуализироваться могут, но полагаться на оптимизатор ни в ООП, ни в ФП, имхо, не стоит.
Только для девиртуализации компилятору надо доказать, что в данном месте других инстансов быть не может, а это задача сильно более нелокальная и сильно более сложная, чем просто подставить параметр, когда он доступен (что выполняется субъективно ИМХО куда чаще, чем условия для девиртуализации, потому что мало кто играется с экзистенциальными типами или полиморфной рекурсией и при этом ожидает должный перф).
Особенно как автор статьи, заявляющий, что вообще всё мономорфизируется.
Это он зря, конечно.
А не осталась, случайно, ссылка?
Последний пост в моём стандалон-блоге.

vkni
03.04.2026 06:52А ты знаешь, где обсуждается проблема нелокальности ленивости? То есть, как с этой нелокальностью бороться, да и вообще делается «постновка задачи».

IUIUIUIUIUIUIUI
03.04.2026 06:52Где обсуждается — не знаю. Но решение как в идрисе (когда ленивость явно помечается как Lazy) выглядит вполне себе дружественным к тайпчекингу и локальным рассуждениям. Ты, по крайней мере, знаешь, что именно у тебя ленивое.

vkni
03.04.2026 06:52И пару слов про microHS черкануть в формате поста можешь?

IUIUIUIUIUIUIUI
03.04.2026 06:52Нет, потому что про него я знаю очень отрывочно — мне напели содержимое презентации, не более.

ruomserg
03.04.2026 06:52Ох ты ж блин - статья является отличной иллюстрацией тезиса: "Человек, впервые взявший в руки молоток - рассматривает все окружающие предметы как разновидности гвоздей" (С).
Да, мы все в курсе, что исчисление функций эквивалентно машине Тьюринга - а значит любая вычислимая задача может быть описана как функционально, так и императивно. То есть - даже мой поход в магазин в субботу можно описать как функциональное преобразование из цифр на банковском счете в набор продуктов в холодильнике...
Вопрос - нахрена, а главное - зачем ?! Если объективно процесс ествественным образом описывается как набор шагов, меняющих состояние системы - то почему бы не последовать совету берестяной грамоты N35, и не описать его на языке программирования тем же (т.е. императивным) способом ? В чем смысл натягивания совы на глобус и вытягивания цепочки лямбд - которые потом еще хрен отладишь нормально (потому что не видны промежуточные преобразования коллекций) ?
Я лично вижу два случая, когда ФП надо применять:
У вас есть большой и полезный фреймворк, который вы можете кастомизировать - и для этого вам надо в глубины этого фреймворка передать определенное поведение. Так вы вот это поведение запихиваете в функцию, и ее как first-class-object передаете в глубины чужого кода. В моем примере с магазином - это как если бы кто-то уже написал фреймворк, автоматизирующий все промежуточные этапы, и надо было только подать внутрь функцию выбора товаров с полок...
Либо у вас функциональное описание является естественным описанием процесса. То есть реально процесс зависит только от входов, не порождает side-effects (логгинг и инструментация весело машут ручкой в этот момент - ибо являются сайд-эффектами), и т.д.
Ну и до кучи скажу что архитектура современных ЭВМ нихрена не заточена под ФП - она заточена под императивное программирование с мутабельным состоянием в RAM. Соответственно, ФП всегда будет подвергаться performance penalty по сравнению с императивным кодом. Другое дело - что производительность софта за последние десятилетия опустили на такое дно, что добавление туда еще и ФП - уже существенно ничего не меняет...

IUIUIUIUIUIUIUI
03.04.2026 06:52То есть - даже мой поход в магазин в субботу можно описать как функциональное преобразование из цифр на банковском счете в набор продуктов в холодильнике… Вопрос - нахрена, а главное - зачем ?! Если объективно процесс ествественным образом описывается как набор шагов, меняющих состояние системы
Даже процесс похода в магазин вы внутри себя описываете декларативно. Вы думаете в терминах того, что надо сделать (выйти из квартиры, вызвать лифт, перейти дорогу, дойти до магазина), а не как это сделать (это будет настолько объёмно, что я даже проверку открытия-закрытия двери писать не буду), и вы притворяетесь, что у ваших действий нет несмоделированных сайд-эффектов: вы не рассматриваете износ петель двери в процессе выхода из дома, выхлопные газы автобуса, смену настроения продавщицы в магазине из-за рисунка на вашей кредитной карточке, и так далее.
Вопрос - нахрена, а главное - зачем ?!
Потому что о чистой функциональщине без неявных сайд-эффектов проще рассуждать и доказывать её свойства.
В чем смысл натягивания совы на глобус и вытягивания цепочки лямбд - которые потом еще хрен отладишь нормально (потому что не видны промежуточные преобразования коллекций) ?
Прелесть в том, что их не нужно отлаживать так часто, как вы думаете :]
Ну и до кучи скажу что архитектура современных ЭВМ нихрена не заточена под ФП - она заточена под императивное программирование с мутабельным состоянием в RAM. Соответственно, ФП всегда будет подвергаться performance penalty по сравнению с императивным кодом.
А почему у меня тогда на хаскеле регулярно получается писать код по производительности на уровне плюсов (иногда — быстрее)?
Потому что нет, конечно, не «всегда будет», это non sequitur. Компилятору ФП никто не мешает компилировать ФП-код во вполне производительный императивный мутабельный код. Более того, начиная с некоторого уровня сложности компиляторов это проще: типизированное ФП (а другое не нужно) выражает в типах больше инвариантов и даёт компилятору больше информации о том, какие оптимизации не изменят семантику программу, и их можно безопасно применить. Просто до этого уровня в императивном (и близком к железу — привет условной джаве) программировании программист делает ту работу, которую может (и должен) делать компилятор.
А вообще забавно то, что я как раз на неделе начал читать сборник публикаций первой конференции History of Programming Languages, и там целый Джон Бакус (который автор Фортрана, куда уж императивнее, и который Backus Normal Form для описания синтаксиса) пишет где-то в окрестности 1980-го года:
My own opinion as to the effect of FORTRAN on later languages and the collective impact of such languages on programming generally is not a popular opinion. […] I now regard all conventional languages (e.g. , the FORTRANs, the ALGOLs, their successors and derivatives) as increasingly complex elaborations of the style of programming dictated by the von Neumann computer. These " von Neumann languages" create enormous, unnecessary intellectual roadblocks in thinking about programs and in creating the higher level combining forms required in a really powerful programming methodology. Von Neumann languages constantly keep our noses pressed in the dirt of address computation and the separate computation of single words, whereas we should be focusing on the form and content of the overall result we are trying to produce. We have come to regard the DO, FOR, WHILE statements and the like as powerful tools, whereas they are in fact weak palliatives that are necessary to make the primitive von Neuman style of programming viable at all.
By splitting programming into a world of expressions on the one hand and a world of statements on the other, von Neumann languages prevent the effective use of higher level combining forms; the lack of the latter makes the definitional capabilities of von Neumann languages so weak that most of their important features cannot be defined-starting with a small, elegant framework-but must be built into the framework of the language at the outset. The gargantuan size of recent von Neumann languages is eloquent proof of their inability to define new constructs: for no one would build in so many complex features if they could be defined and would fit into the existing framework later on.
The world of expressions has some elegant and useful mathematical properties whereas the world of statements is a disorderly one, without useful mathematical properties. Structured programming can be viewed as a modest effort to introduce a small amount of order into the chaotic world of statements. The work of Hoare ( 1 969), Dijkstra ( 1 976), and others to axiomatize the properties of the statement world can be viewed as a valiant and effective effort to be precise about those properties, ungainly as they may be.
This is not the place for me to elaborate any further my views about von Neumann languages. My point is this: while it was perhaps natural and inevitable that languages like FORTRAN and its successors should have developed out of the concept of the von Neumann computer as they did, the fact that such languages have dominated our thinking for twenty years is unfortunate. It is unfortunate because their long-standing familiarity will make it hard for us to understand and adopt new programming styles which one day will offer far greater intellectual and computational power.

ruomserg
03.04.2026 06:52Вот вы удивитесь - но если я кому-то буду рассказывать как я хожу в магазин - то это будет именно пошаговое перечисление действий: одеваюсь и оказываюсь одет, выхожу и оказываюсь на улице, двигаюсь в разных направлениях по улицам и перекресткам - оказываюсь у дверей магазина, набираю продукты, расплачиваюсь на кассе, и т.д. А вот сайд-эффектами этой деятельности оказываются a) уменьшившееся количество денег на счете и b) увеличившееся количество продуктов в холодильнике. И да, можно дискутировать о том до какой степени детализовать этот процесс (в программировании мы обычно детализируем до stdlib call или syscall). Без шуток - покажите мне человека который будет описывать поход в магазин как функциональное преобразование цифр в продукты... "А папа-мама не сумасшедши ли ?" (C) Мультик
Дальше - аргументы о том, что проще рассуждать и доказывать. Охотно верю - но знаете, идите на матфак работать с такими аргументами, что-ли ? Там любят таких... А я тут у мамы инженер. Мне не надо чтобы красиво доказано - мне надо чтобы работало, и легко читалось. Понятно что когда у меня есть streams, то ими надо пользоваться - например не писать свой алгоритм сортировки, а передать лямбду в библиотечный sort(). А когда один черт в коде клеится весь алгоритм, но пересыпается .apply() .with() .orElse() и прочей матерщиной (привет Kotlin!) - очень хочется дать по башке, и отправить копию новгородской грамоты чтобы лучше доходило...
Аргумент про то, чтобы компилировать ФП в мутабельный код - это из серии "можно, но зачем?!". То есть сначала мы сношаем мозг чтобы императивный процесс реального мира описать эквивалентом в мире чистых функций, а потом ждем чтобы компилятор обратно превратил это в императивный код с мутабельными объектами... Я даже теряюсь что предложить в таком случае... Например, когда в следующий раз захотите в туалет - не идите сразу у себя дома, а не поленитесь и дойдите пешком до центра. Потом такси вас обратно привезет - и только потом на толчок... После эксперимента задайте себе вопрос - стоило ли оно того, чтобы оказаться в том же месте по такой кружной траектории ?

9lLLLepuLLa
03.04.2026 06:52Сравнение желтого с вонючим. Кмк любому очевидно, что писать императивный код человеком и трансформировать математически доказанный код в императивный самой машиной - это две большие разницы.

IUIUIUIUIUIUIUI
03.04.2026 06:52Вот вы удивитесь - но если я кому-то буду рассказывать как я хожу в магазин - то это будет именно пошаговое перечисление действий: одеваюсь и оказываюсь одет
Это пошаговое описание именно в декларативном стиле. Ровно в том же смысле, как и, возвращаясь к программированию, «получить множество авторов статей в хабе “программирование” можно через
nubOrd . map author . filter (λpost → "программирование" `elem` hubs post)». Вы можете переубедить меня, описав процесс надевания куртки в императивном стиле, с мутабельными переменными, циклами, явными условиями, и так далее. Чем меньше абстракций уровня «просовываю руку в рукав», тем ближе к императивщине.Практически все попытки выше ассемблера (включая макроассемблер), все эти функции, ООП, структурное программирование, прочее — это попытки разной степени успешности выйти из императивно-детализированной концепции в декларативную. Эти все вот ООП-шаблоны, билдеры там, фабрики, стратегии — это попытка эмулировать декларативщину в императивной среде.
Без шуток - покажите мне человека который будет описывать поход в магазин как функциональное преобразование цифр в продукты… “А папа-мама не сумасшедши ли ?”
Сами придумали тезис («ФП/декларативщина — это когда поход в магазин представляется преобразованием цифр в продукты»), сами его оспариваете, сами победили. Как это называется? Это называется strawman argument.
Олсо, это ведь работает во все стороны: сколько людей описывает поход в магазин как ООП? Сколько людей… инстанциирует класс бумажника? обращается к синглтону бумажников? использует абстрактный класс ИбуМажник? Чей метод вызывается при обмене денег на товар — бумажника, денег, товара, продавца, или абстрактного менеджера сделок? Вы когда ребёнку будете объяснять процесс похода в магазин, будете рассказывать про абстрактный менеджер сделок?
Можно ли сделать вывод в вашем мире, что ООП — это академический зашквар?
Охотно верю - но знаете, идите на матфак работать с такими аргументами, что-ли ? Там любят таких… А я тут у мамы инженер.
Я нигде не говорил о красоте доказательства, я говорил о простоте и возможности. Императивный код особо не доказывают, там херак-херак и в продакшен обычно. Это да, это в императивщине и нетипизированной функциональщине делать проще.
Но я как-то всё-таки хотел бы использовать слово «инженер» как похвалу, а не как уничижительный термин.
Мне не надо чтобы красиво доказано - мне надо чтобы работало, и легко читалось.
И это быстрее происходит в типизированной функциональщине.
А когда один черт в коде клеится весь алгоритм, но пересыпается .apply() .with() .orElse() и прочей матерщиной (привет Kotlin!) - очень хочется дать по башке, и отправить копию новгородской грамоты чтобы лучше доходило…
Товарищ взял не предназначенный для ФП язык и удивляется, что ФП выглядит в нём паршиво. Ну, я не знаю, попробуйте в ООП-стиле на ассемблере писать и потом ругайтесь, что ерунда выходит, клятый ООП.
Аргумент про то, чтобы компилировать ФП в мутабельный код - это из серии “можно, но зачем?!”.
Оптимизирующие компиляторы вы не приемлите вообще, я так понимаю, или это только применительно к ФП такое неприятие?

ruomserg
03.04.2026 06:52Ну как бы если любое движение от ассемблера считать движением в сторону ФП - то дальше спор смысла не имеет. Просто живите там у себя в мире математических абстракций, и даст бог в реальной жизни я вас не встречу.
Я видел развитие языков программирования через руки - начиная от 8-бит ассемблера, и дальше через 80x86 к C, C++, Java и так далее. Никакого движения в сторону FP там не было. Есть объекты реального мира. У них объективно есть состояние, которое меняется со временем по присущим им законам - или под внешним воздействием. В программе мы строим модель этого объекта или явления, отображая законы реального мира в их подобия в модельном пространстве. То-есть заменяем реальный вес на последовательность битов, его кодирующую (с фиксированной или плавающей точкой), положение объекта в пространстве на значения эйлеровых углов (или чего-то посложнее), и так далее. На низком уровне (ассемблере) - не существует способов выразить в языке структуру модели - какая ячейка или регистр за что отвечает - приходится держать в голове. Все дальнейшие нормальные языки: от прости господи, бейсика - до джавы - вводили разные изобратительные средства чтобы отразить объективно существующую структуру реального мира в программе с минимальными искажениями. То есть - группировать признаки относящиеся к одному модельному объекту в структуры, и сделать сложнее некорректные действия - типа вызвать метод "покрутить хвостом" к кухонной двери вместо кошки. Никто, щука, никогда не пытался представить модель мира как комбинацию бесчисленного количества чистых функций.
При том, что интеллектуально я готов принять множественность описания объектов реального мира (например, описание сигналов в частотной области спектрами, или во временной - графиками) - идея описать всё сущее чистыми функциями - проходит в моей канцелярии как забавный курьез. Ну и что, что можно ?! Вон метафора конечных автоматов тоже полна по Тьюирнгу - и любой процесс или явление можно описать таким образом. И есть чудаки которые до сих пор носятся с концепцией автоматного программирования. И аргументы у них точно такие же как у вас: это простой формализм, компилятору легче оптимизировать, и т.д. Проблема в том, что существует все-таки естественное описание объекта, которое является предпочтительным. Есть природные явления которые прямо-таки созданы для описания КА (или ФП). И там их надо применять. А там где это не естественно - там начинаются натягивания совы на глобус: давайте создадим скрытые фиктивные состояния, давайте добавим фиктивные входные и выходные сигналы, и т.д.
В итоге - повторю свой изначальный тезис: то что у вас появился в руках молоток - не делает остальные предметы гвоздями! Пожалуйста не надо хреначить направо и налево - да еще и похваляться этим...

Siemargl
03.04.2026 06:52Любая абстракция лишь примитивное упрощение реальности мира.
В этом смысле они равноудалены и не особо лучше одна другой)

qweqweqweqweqweqweqweqwe
03.04.2026 06:52Олсо, это ведь работает во все стороны: сколько людей описывает поход в магазин как ООП? Сколько людей… инстанциирует класс бумажника? обращается к синглтону бумажников? использует абстрактный класс ИбуМажник? Чей метод вызывается при обмене денег на товар — бумажника, денег, товара, продавца, или абстрактного менеджера сделок? Вы когда ребёнку будете объяснять процесс похода в магазин, будете рассказывать про абстрактный менеджер сделок?
Ха-ха-ха Execution in the Kingdom of Nouns - 20 лет прошло, а ничего не поменялось!
Символическое в ООП — более воображаемое и нарративное. Оно создаёт красивую историю: «объекты взаимодействуют, как люди/вещи в мире».
ФП требует абстрактного, математического мышления: данные отдельно, поведение (функции) отдельно, всё immutable. Это мощно, но когнитивно дороже для большинства.
Моя голова устает сильнее когда я программирую на ФП.

winkyBrain
03.04.2026 06:52Расплодилось великое множество объектно-ориентированных языков: C++, Java, C#, JavaScript
На последнем очень хочется остановиться) можно подумать, что это ошибка, но нет, дальше в списке будет и TypeScript, так что нет, автор явно намеренно записал JavaScript в расплодившиеся объектно-ориентированные языки с начала 90-х.
А теперь следим за руками:
Официальная дата запуска JavaScript - 4 декабря 1995 года.
Классы в JavaScript появились в ECMAScript 2015 (ES6), выпущенном в июне 2015 года.
И даже в таком случае это тоже функции, объявляемые иначе, о чём прямо сказано на MDN -
Classes are in fact “special functions”, and just as you can define function expressions and function declarations, a class can be defined in two ways: a class expression or a class declaration
Ничего себе, расплодившийся с начала 90-х ООПшный язык, в котором до 2015 года даже не существовало ключевого слова class) Ну и смысл читать дальше после подобных заявлений? Человек же явно не понимает, о чём говорит

orenty7
03.04.2026 06:52Ничего себе, расплодившийся с начала 90-х ООПшный язык, в котором до 2015 года даже не существовало ключевого слова class) Ну и смысл читать дальше после подобных заявлений? Человек же явно не понимает, о чём говорит
Самоиронично выходит, если знать, что JS это действительно объектно-ориентированный язык (и всегда им был), просто использующий прототипное наследование вместо классов

amcured
03.04.2026 06:52Терминология — мать порядка.
Начнем с того, что у термина «объектно-ориентированный язык» есть два практически прямо противоположных друг другу значения: аланокаевское и джеймсоногослинговое.
Вика предлагает определение, списанное под копирку у Лема из «Дневников Ййона Тихого»:
Объектно-ориентированный язык программирования (ОО-язык) — язык, построенный на принципах объектно-ориентированного программирования.
Инстансы тайп-классов в хаскеле очень сложно не назвать объектами по Гослингу.
И вообще, какой язык называть «объектно-ориентированным», а какой «функциональным» в 2026 году, когда в джаве уже не продохнуть от лямбд и ленивых стримов — это теософский спор, как про табы и пробелы.

orenty7
03.04.2026 06:52Объектно-ориентированный язык программирования (ОО-язык) — язык, построенный на принципах объектно-ориентированного программирования.
Там если по ссылке пройти, то есть набор основных понятий ООП: абстракция данных, инкапсуляция, наследование, полиморфизм подтипов, класс, объект. Абстракции данных в хаскеле нет, сокрытие происходит на уровне модулей; инкапсуляции нет, методы и данные живут отдельно; наследования нет; полиморфизма подтипов нет, ну потому что подтипов нет.
Инстансы тайп-классов в хаскеле очень сложно не назвать объектами по Гослингу.
Раскройте мысль, пожалуйста
И вообще, какой язык называть «объектно-ориентированным», а какой «функциональным» в 2026 году, когда в джаве уже не продохнуть от лямбд и ленивых стримов — это теософский спор, как про табы и пробелы.
Язык может быть мультипарадигменным, противоречий тут нет. Если говорить конкретно про джаву, разве лямбды там это не просто синтаксический сахар для анонимного класса?

9lLLLepuLLa
03.04.2026 06:52Уже давно нет (к вопросу про джаву)

kmatveev
03.04.2026 06:52С точки зрения семантики языка - да, это сахар для анонимного класса, реализующего функциональный интерфейс. Детали реализации (MethodHandle, invokedynamic) этого не меняют.

amcured
03.04.2026 06:52набор основных понятий ООП: абстракция данных, инкапсуляция, наследование, полиморфизм подтипов, класс, объект
Давайте я еще раз попробую. «Основных понятий ООП» не существует в аксиоматике нашего мира. Алан Кай под ООП подразумевал одно, Гослинг — совершенно другое. Тут уместно вспомнить еще Матца, доведшего идею «everything is an object» до абсурда в руби.
Потом еще на горизонте появился Боб Мартин, со своими мухоморовыми галлюцинациями. Графомания этого инфоцыгана, в жизни не написавшего ни строки серьёзного кода, внезапно была воздвигнута на хоругви (потому что люди en masse туповаты, и без шор и узды в приниципе ни на что не способны). В результате у нас появилось определение ООП размером не менее трех печатных листов.
Инкапсуляция, полиморфизм и еще добрый десяток «основных понятий ООП» — прекрасно доступны во всех языках, от КОБОЛа до Хаскеля. Остальные — наследование, классы и прочие паттерны — никому нафиг не нужны (единственное разумное «наследование» вместо правильного решения — полиморфизма — реализовано в джаваскрипте).
То ООП, которое придумал Кай ко всему этому никакого отношения не имеет. То, которое предлагал Матц — тем более. Гослинг ну кхм.
В этом смысле, если выбросить в мусорную корзину бобмартиновщину и фаулерщину, — то инстансы тайп-классов в хаскеле — это хорошие, правильные объекты.

orenty7
03.04.2026 06:52Давайте я еще раз попробую. «Основных понятий ООП» не существует в аксиоматике нашего мира. Алан Кай под ООП подразумевал одно, Гослинг — совершенно другое. Тут уместно вспомнить еще Матца, доведшего идею «everything is an object» до абсурда в руби.
Я привожу википедийное определение, которое более-менее совпадает с общепринятым. Утверждений о его единственности или универсальности я не делаю.
Инкапсуляция, полиморфизм и еще добрый десяток «основных понятий ООП» — прекрасно доступны во всех языках, от КОБОЛа до Хаскеля.
ООП-шной инкапсуляции в хаскеле нет: методы и данные живут отдельно, сокрытие происходит на уровне модулей. Сами модули считать объектами нельзя, поскольку в хаскеле они, в отличие от окамла, не first-class. Полиморфизмы тоже другие: в ООП полиморфизм подтипов, у хаскеля полиморфизмы параметрический и ad-hoc.
На всякий случай проговорю, что я не спорю с тем, что в хаскеле можно писать в ОО-стиле. Это можно делать едва ли не в любом языке. Я спорю с тем, что хаскель это объектно-ориентированный ЯП.
В этом смысле, если выбросить в мусорную корзину бобмартиновщину и фаулерщину, — то инстансы тайп-классов в хаскеле — это хорошие, правильные объекты.
Не могли бы вы привести пару примеров с использованием инстансов как объектов? Простенький счётчик с методами increment/decrement, объекты круга и квадрата с общим методом area, и тому подобное

amcured
03.04.2026 06:52википедийное определение, которое более-менее совпадает с общепринятым
«Обще-» это кем конкретно?
ООП-шной инкапсуляции в хаскеле нет […]
Я прочитал «Введение в Хаскель за 7 часов» и знаю на базовом уровне, как там устроена инкапсуляция. А вот что такое «ООП-шная инкапсуляция», простите уж, я не в курсе. Это какая-то особенная инкапсуляция? С блекджеком и методистками?
модули считать объектами нельзя, поскольку в хаскеле они, в отличие от окамла, не first-class
Ого! Окамл-то тут при чем? В Окамле что же, настоящее православное общепринятое ООП?
Я спорю с тем, что хаскель это объектно-ориентированный ЯП.
Очень за вас рад. А кто именно такой тезис высказал? Может быть, нам этого человека тоже позвать?
объекты круга и квадрата с общим методом area
Полиморфизм, реализованный через наследование, — это вообще никаким боком не часть парадигмы, это детали имплементации. Вы просите, чтобы вам повторили слово в слово джавовый код на хаскеле, а лучше бы попытались понять, о чем вообще талдычит собеседник.

orenty7
03.04.2026 06:52«Обще-» это кем конкретно?
https://ru.wiktionary.org/wiki/общепринятый
Удивительные провалы в памяти, когда “графомания была воздвигнута на хоругви” или “наследование, классы и прочие паттерны — никому нафиг не нужны” вы понимаете о ком речь, а когда “общепринятое определение”, то нет. Удобно, что сказать.
Я прочитал «Введение в Хаскель за 7 часов» и знаю на базовом уровне, как там устроена инкапсуляция. А вот что такое «ООП-шная инкапсуляция», простите уж, я не в курсе. Это какая-то особенная инкапсуляция? С блекджеком и методистками?
Вы под инкапсуляцией явно понимаете что-то отличное от того, что обычно имеют ввиду, потому что обычной, общепринятой инкапсуляции в хаскеле нету. Уточнение “ООП-шная” просто это подчёркивает.
Ого! Окамл-то тут при чем?
Обычное сравнение со схожим языком. Не понимаю почему вы так взъелись.
В Окамле что же, настоящее православное общепринятое ООП?
Учитывая, что OCaml раньше назывался “Objective Caml”, подозреваю, что да
Очень за вас рад. А кто именно такой тезис высказал? Может быть, нам этого человека тоже позвать?
Цитирую:
Инкапсуляция, полиморфизм и еще добрый десяток «основных понятий ООП» — прекрасно доступны во всех языках, от КОБОЛа до Хаскеля. Остальные — наследование, классы и прочие паттерны — никому нафиг не нужны
Как вы думаете, является ли язык, в котором доступны десяток основных понятий ООП объектно-ориентированным?
Полиморфизм, реализованный через наследование, — это вообще никаким боком не часть парадигмы, это детали имплементации.
Давайте я просто вашими фразами и отвечу:
Очень за вас рад. А кто именно реализовывать через наследование просил? Может быть, нам этого человека тоже позвать?
Вы просите, чтобы вам повторили слово в слово джавовый код на хаскеле, а лучше бы попытались понять, о чем вообще талдычит собеседник.
Я вот уже третий комментарий пытаюсь от собеседника добиться куска кода подтверждающего тезис “инстансы тайп-классов в хаскеле очень сложно не назвать объектами по Гослингу”, но ему, видимо, приятнее пофилософствовать про ООП, дядю Боба и т.д., вместо того, чтобы реально привести код.

amcured
03.04.2026 06:52А кто именно реализовывать через наследование просил?
Мне неизвестны иные способы «привести пару примеров […] объекты круга и квадрата с общим методом
area».Если имелось в виду не с «общим методом», а «каждый с методом area» — уверен, вы сами осилите написать такой пример на хаскеле.
третий комментарий пытаюсь
Второй, но я уже заметил, что вам до фени факты,
ему, видимо, приятнее пофилософствовать про ООП, дядю Боба и т.д., вместо того, чтобы реально привести код
Естественно. Это же хабр, а не профессиональный форум. Убеждать лично вас мне без надобности, писать тривиальный код, который бесплатный дипсик за десять секунд может нагенерировать — тем более.
Я развлекаюсь, глядя, как вы пыжитесь. Для этого и существует хабр, разве нет?

ptr128
03.04.2026 06:52Почему-то автор рассматривает только написание кода, но не весь его жизненный цикл. ООП при всех его недостатках упрощает и удешевляет развитие программного продукта. А в Rust даже простое добавление нового поля в структуру может потребовать либо дублирования кода, либо массового переписывания трейтов.

v_0ver
03.04.2026 06:52Не понял, если просто добавить поле в структуру, то какое дублирование нужно? В метод конструктор в
Self{}дописать поле?)
И какие вы трейты собрались переписывать, когда в трейтах нельзя получить доступ к полю структуры?) Если вы имели ввиде переписывать имплементации, то это ничем не больше чем переписывать теже имплантации в любом ООП языке.
ptr128
03.04.2026 06:52И какие вы трейты собрались переписывать, когда в трейтах нельзя получить доступ к полю структуры
Типаж это не только его описание, но так же и его имплементация. Они неотделимы.
это ничем не больше чем переписывать теже имплантации в любом ООП языке
За тем исключением, что в ООП можно унаследовать классы, а не переписывать как их, так и код, их использующий. Если в каких то методах класса иногда может потребоваться новый параметр, связанный с новым свойством (полем структуры в Rust), то достаточно перегрузить только эти методы.
Особенно ярко это проблема на Rust проявляется при динамическом связывании.

xakepp35
03.04.2026 06:52Я полагаю, что в серьезных mission-critical системах нужно уходить от fp и stack-return-based моделей языков, в пользу полностью императивных процедурных(no-return) event-driven систем. Tl;dr Можно было бы сгенерить статейку почему wal-executor(fifo) круче стека(lifo+забыли залогать) для какого нибудь финтеха или миссии на марс :)

A1exMa
03.04.2026 06:52А я вот понимаю восторги автора, потому что и сам в детстве был укушен джавой и подхватил ООП головного мозга. Чтобы вылечиться, несколько лет ушло, а когда я дошел до ФП, моей радости не было предела (и до сих пор я от щастья плачу)
Ну что вы накинулись на человека? Дайте ему порадоваться. Со временем, он сам поймет, что это не панацея, и где-то он превратно что-то понял
Но сейчас-то - вы посмотрите сколько радости! А примеры какие живые - старался...
onuchin
Всегда умиляли эти теоретики от программирования, которые не закончили ни одного проекта.
Всегда хочется спросить - "когда ты написал свою миллионную LOC?"
Автор не упоминает LINQ и ему подобных, которые стали частью языка.
Siemargl
Дополню в ту же тему
Проблема Х
Все преимущества Раста показываются на примерах уровня хелловорлда - пара строк кода. И эта статья не исключение.
А если мы начинаем смотреть реальные проекты на Раст - "там водятся тигры" (С)
Я не говорю, что Раст совсем плохой - наоборот, в нем многое сделано хорошо (да, но далеко не все). Но показать это надо правильно - на реальных задачах.