[Перевод] Можно ли осознанно отказаться от функционального программирования?
Функциональное программирование пронизывает большую часть основного мира программирования — экосистема JavaScript, Linq для C#, даже функции высокого порядка в Java. Так выглядит Java в 2018-м:
getUserName(users, user -> user.getUserName());
Функциональное программирование настолько полезно и удобно, что, насколько я вижу, проникло во все современные распространённые языки.
Но не всё так радужно. Многие разработчики сопротивляются этому тектоническому сдвигу в нашем подходе к ПО. Честно говоря, сегодня трудно найти работу, связанную с JavaScript, которая не требует знания концепций ФП.
Функциональное программирование лежит в основе обоих доминирующих фреймворков: React (архитектура и односторонняя передача данных создавались с целью избежать общего изменяемого DOM) и Angular (RxJS — широко используемая по всему фреймворку библиотека utility-операторов, которые работают с потоками посредством функций высшего порядка). Redux и ngrx/store тоже являются функциональными.
Новичков может пугать подход ФП, и ради быстрого втягивания в работу кто-нибудь из вашей команды может предложить освоиться с кодовой базой, отказавшись от ФП.
Для менеджеров, незнакомых с самим ФП или его влиянием на современную экосистему программирования, такое предложение может показаться разумным. В конце концов, разве ООП не служит нам верой и правдой 30 лет? Почему не оставить всё как есть?
Давайте разберёмся с самой формулировкой. Что вообще означает «бан» ФП на уровне политики?
Что такое функциональное программирование?
Моё любимое определение:
Функциональное программирование — это парадигма использования чистых функций в качестве неделимых единиц композиции, с избеганием общего изменяемого состояния и побочных эффектов.
Чистая функция:
- Получая одни и те же входные данные, всегда возвращает один и тот же результат
- Не имеет побочных эффектов
То есть суть ФП сводится к:
- Программированию с функциями
- Избеганию общего изменяемого состояния и побочных эффектов
Сложив всё вместе, мы получаем разработку ПО со строительными блоками в виде чистых функций (неделимые единицы композиции).
К слову, Алан Кэй (основоположник современного ООП) считает, что суть объектно-ориентированного программирования заключается в:
- Инкапсуляции
- Передаче сообщений
Так что ООП просто ещё один способ избежать общего изменяемого состояния и побочных эффектов.
Очевидно, что противоположностью ФП является не ООП, а неструктурированное, процедурное программирование.
Язык Smalltalk (в котором Алан Кэй заложил основы ООП) одновременно объектно-ориентированный и функциональный, а необходимость выбирать что-то одно является чуждой и неприемлемой идеей.
То же самое верно и для JavaScript. Когда Брендана Эйха наняли разрабатывать этот язык, то основной его идеей было создать:
- Scheme для браузера (ФП)
- Язык, который выглядит как Java (ООП)
В JavaScript вы можете попытаться придерживаться какой-то одной парадигмы, но, к добру или нет, обе они неразрывно связаны. Инкапсуляция в JavaScript зависит от замыканий — концепции из ФП.
Наверняка вы уже применяете функциональное программирование, быть может даже не зная об этом.
Как НЕ использовать ФП
Чтобы избежать функционального программирования, вам придётся избегать использования чистых функций. Но тогда вы не сможете писать подобный код, потому что он может оказаться чистым:
const getName = obj => obj.name;
const name = getName({ uid: '123', name: 'Banksy' }); // Banksy
Давайте рефакторим код так, чтобы он больше не относился к ФП. Можно сделать класс с публичным свойством. Поскольку инкапсуляция не используется, было бы преувеличением назвать это ООП. Возможно, «процедурное объектное программирование»?
class User {
constructor ({name}) {
this.name = name;
}
getName () {
return this.name;
}
}
const myUser = new User({ uid: '123', name: 'Banksy' });
const name = myUser.getName(); // Banksy
Поздравляю! Только что мы превратили 2 строки кода в 11, а также привнесли возможность появления неконтролируемой внешней мутации. А что получили взамен?
Ну, вообще-то, ничего. Фактически, мы потеряли в универсальности и возможности значительной экономии кода.
Предыдущая функция getName () работала с любым входящим объектом. Новая функция тоже работает (потому что это JS и мы можем делегировать методы любым объектам), но получается гораздо неуклюжей. Можно позволить двум классам наследовать от обычного родительского класса, но это подразумевает наличие ненужных взаимоотношений между классами.
Забудьте о многократном использовании. Мы только что спустили его в унитаз. Так выглядит дублирование кода:
class Friend {
constructor ({name}) {
this.name = name;
}
getName () {
return this.name;
}
}
Старательный ученик с задней парты воскликнет: «Ну так создайте класс person!»
Тогда:
class Country {
constructor ({name}) {
this.name = name;
}
getName () {
return this.name;
}
}
«Но это же разные типы. Конечно, вы не можете применять метод класса person к стране!».
На это я отвечу: «А почему нет?»
Одним из невероятных преимуществ функционального программирования является тривиальная простота программирования обобщённого. Можно назвать это «обобщённость по умолчанию»: возможность написать одну функцию, которая будет работать с любым типом, удовлетворяющим его обобщённым требованиям.
Примечание для Java-программистов: речь не идёт о статической типизации. Некоторые ФП-языки имеют прекрасные системы статических типов, но всё же пользуются преимуществами структурированных типов и/или HKT (higher-kinded types).
Это очевидный пример, но получаемая с помощью этого трюка экономия объёма кода получается огромной.
Это позволяет библиотекам вроде autodux автоматически генерировать доменную логику для любого объекта, созданного из пары геттер/сеттер (и многих других). Также мы можем уменьшить объём кода доменной логики вдвое, а то и больше.
Больше никаких функций высшего порядка
Поскольку многие (но не все) функции высшего порядка пользуются преимуществами чистых функций для возврата одних и тех же значений при получении одних и тех же входных данных, вы не можете без возникновения побочных эффектов использовать функции вроде .map(), .filter(), .reduce()
:
const arr = [1,2,3];
const double = n => n * 2;
const doubledArr = arr.map(double);
Становится:
const arr = [1,2,3];
const double = (n, i) => {
console.log('Random side-effect for no reason.');
console.log('Oh, I know, we could directly save the output to the database and tightly couple our domain logic to our I/O. That will be fine. Nobody else will need to multiply by 2, right?');
saveToDB(i, n);
return n * 2;
};
const doubledArr = arr.map(double);
Покойся с миром, композиция функции. 1958–2018
Забудьте о point-free-композиции компонентов высшего порядка для инкапсулирования сквозной функциональности на страницах. Этот удобный, декларативный синтаксис превращается в табу:
const wrapEveryPage = compose(
withRedux,
withEnv,
withLoader,
withTheme,
withLayout,
withFeatures({ initialFeatures })
);
Вам придётся всё это вручную импортировать в каждый компонент, а то и хуже — погружаться в запутанную, негибкую иерархию с наследованием классов (которую большинство сознательных граждан, и даже (особенно?) канон объектно-ориентированного программирования справедливо признают антипаттерном).
Прощайте, промисы и async/await
Промисы — это монады. Технически, они относятся к теории категорий, но я слышал, что промисы всё же относятся к ФП, также потому что в Haskell они используются для обеспечения чистоты и ленивости (lazy).
Честно говоря, будет не так плохо избавиться от монад и функторов. Они гораздо проще, чем мы их выставляем! Я не просто так учу людей использовать Array.prototype.map
и промисы до того, как рассказываю об общих концепциях функторов и монад.
Знаете, как их применять? Значит, вы уже на полпути к пониманию функторов и монад.
Итак, чтобы избежать функционального программирования
- Не используйте популярные JavaScript-фреймворки и библиотеки (все они предадут вас ради ФП!)
- Не пишите чистые функции
- Не используйте многие из встроенных возможностей JavaScript: большинство математических функций (потому что они чистые), неизменяемые методы строк и массивов,
.map(), .filter(), .forEach()
, промисы или async/await - Пишите ненужные классы
- Удваивайте (или ещё больше увеличивайте) доменную логику, вручную набивая геттеры и сеттеры буквально для всего подряд
- Возьмите «читабельный, явный» императивный подход и запутайте доменную логику задачами отрисовки и сетевого ввода-вывода
И попрощайтесь с:
- Time travel-отладкой
- Простой разработкой фич undo/redo
- Надёжными, согласованными модульными тестами
- Заглушками и тестированием без D/I
- Быстрыми модульными тестами без зависимостей с сетевым вводом-выводом
- Маленькими кодовыми базами, удобными для тестирования, отладки и сопровождения
Избегаете функционального программирования? Без проблем.
Или погодите…