Хабрахабр

[Перевод] Можно ли осознанно отказаться от функционального программирования?

Функциональное программирование пронизывает большую часть основного мира программирования — экосистема JavaScript, Linq для C#, даже функции высокого порядка в Java. Так выглядит Java в 2018-м:

getUserName(users, user -> user.getUserName());

Функциональное программирование настолько полезно и удобно, что, насколько я вижу, проникло во все современные распространённые языки.

Многие разработчики сопротивляются этому тектоническому сдвигу в нашем подходе к ПО. Но не всё так радужно. Redux и ngrx/store тоже являются функциональными. Честно говоря, сегодня трудно найти работу, связанную с JavaScript, которая не требует знания концепций ФП.
Функциональное программирование лежит в основе обоих доминирующих фреймворков: React (архитектура и односторонняя передача данных создавались с целью избежать общего изменяемого DOM) и Angular (RxJS — широко используемая по всему фреймворку библиотека utility-операторов, которые работают с потоками посредством функций высшего порядка).

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

В конце концов, разве ООП не служит нам верой и правдой 30 лет? Для менеджеров, незнакомых с самим ФП или его влиянием на современную экосистему программирования, такое предложение может показаться разумным. Почему не оставить всё как есть?

Что вообще означает «бан» ФП на уровне политики? Давайте разберёмся с самой формулировкой.

Что такое функциональное программирование?

Моё любимое определение:

Функциональное программирование — это парадигма использования чистых функций в качестве неделимых единиц композиции, с избеганием общего изменяемого состояния и побочных эффектов.

Чистая функция:

  • Получая одни и те же входные данные, всегда возвращает один и тот же результат
  • Не имеет побочных эффектов

То есть суть ФП сводится к:

  • Программированию с функциями
  • Избеганию общего изменяемого состояния и побочных эффектов

Сложив всё вместе, мы получаем разработку ПО со строительными блоками в виде чистых функций (неделимые единицы композиции).

К слову, Алан Кэй (основоположник современного ООП) считает, что суть объектно-ориентированного программирования заключается в:

  • Инкапсуляции
  • Передаче сообщений

Так что ООП просто ещё один способ избежать общего изменяемого состояния и побочных эффектов.

Очевидно, что противоположностью ФП является не ООП, а неструктурированное, процедурное программирование.

Язык Smalltalk (в котором Алан Кэй заложил основы ООП) одновременно объектно-ориентированный и функциональный, а необходимость выбирать что-то одно является чуждой и неприемлемой идеей.

Когда Брендана Эйха наняли разрабатывать этот язык, то основной его идеей было создать: То же самое верно и для JavaScript.

  • Scheme для браузера (ФП)
  • Язык, который выглядит как Java (ООП)

В JavaScript вы можете попытаться придерживаться какой-то одной парадигмы, но, к добру или нет, обе они неразрывно связаны. Инкапсуляция в JavaScript зависит от замыканий — концепции из ФП.

Наверняка вы уже применяете функциональное программирование, быть может даже не зная об этом.

Как НЕ использовать ФП

Чтобы избежать функционального программирования, вам придётся избегать использования чистых функций. Но тогда вы не сможете писать подобный код, потому что он может оказаться чистым:

const getName = obj => obj.name;
const name = getName(); // 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, а также привнесли возможность появления неконтролируемой внешней мутации. А что получили взамен?

Фактически, мы потеряли в универсальности и возможности значительной экономии кода. Ну, вообще-то, ничего.

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

Мы только что спустили его в унитаз. Забудьте о многократном использовании. Так выглядит дублирование кода:


class Friend { constructor ({name}) { this.name = name; } getName () { return this.name; }
}

Старательный ученик с задней парты воскликнет: «Ну так создайте класс person!»

Тогда:

class Country { constructor ({name}) { this.name = name; } getName () { return this.name; }
}

«Но это же разные типы. Конечно, вы не можете применять метод класса person к стране!».

На это я отвечу: «А почему нет?»

Можно назвать это «обобщённость по умолчанию»: возможность написать одну функцию, которая будет работать с любым типом, удовлетворяющим его обобщённым требованиям. Одним из невероятных преимуществ функционального программирования является тривиальная простота программирования обобщённого.

Некоторые ФП-языки имеют прекрасные системы статических типов, но всё же пользуются преимуществами структурированных типов и/или HKT(higher-kinded types). Примечание для Java-программистов: речь не идёт о статической типизации.

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

Также мы можем уменьшить объём кода доменной логики вдвое, а то и больше. Это позволяет библиотекам вроде 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
  • Быстрыми модульными тестами без зависимостей с сетевым вводом-выводом
  • Маленькими кодовыми базами, удобными для тестирования, отладки и сопровождения

Избегаете функционального программирования? Без проблем.
Или погодите…

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть