Можно ли избежать функционального программирования?

Vlad Poe
5 min readApr 23, 2018

--

Перевод статьи «Can You Avoid Functional Programming as a Policy? » Эрика Эллиота.

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

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

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

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

Два доминирующих на рынке фреймворка вдохновлены функциональной парадигмой. Это React, однонаправленный data-flow и архитектура которого направлены против разделяемой мутируемости DOM’a. И это Angular, который гармонично совместим с RxJS — библиотекой для управления потоком данных посредством функций высшего порядка. В эту же сторону смотрят Redux и ngrx/store — “функциональные” по всем статьям.

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

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

Я приглашаю вас представить, что все именно так. Давайте посмотрим, что будет значить осознанный отказ от ФП?

Функциональное программирование, что это такое?

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

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

Чистая функция — это функция, которая:

  • при заданных аргументах всегда возвращает идентичное значение
  • не имеет побочных эффектов.

По большому счету, суть ФП сводится к следующему:

  • Функция — основная единица кода
  • Неизменяемое/иммутабельное состояние, отсутствие побочных эффектов

Сложим это воедино и получим программирование, суть которого в написании чистых функций, являющимися строительными блоками программы (“базовыми единицами композиции”).

Кстати, согласно Алану Кею, родоначальнику современного ООП, суть объектно-ориентированного программирования в

  • Инкапсуляции
  • Обмене сообщениями

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

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

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

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

  • своего рода Scheme для браузеров (ФП)
  • язык, похожий на Java (ООП)

JavaScript позволяет следовать и первому и второму подходу, которые, нравится нам это или нет, неразрывно связаны. Инкапсуляция в JS построена на замыканиях — концепции, пришедшей из функционального программирования.

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

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

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

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() работал с любым объектом-аргументом. Текущий тоже позволяет делать это (так как в JavaScript можно делегировать методы любым другим объектам), но намного неудобнее. Да, мы можем заставить два класса наследоваться от некоего родительского класса, но это создаст зависимость, которой, скорее всего, вообще не должно быть.

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

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, автоматически генерировать логику приложений в работе с объектами, состоящими из пар getter/setter. А это только малая часть возможностей. Код сокращается более чем вдвое.

Никаких функций высшего порядка

Так как большинство (не все) функций высшего порядка чистые (возвращают значение, зависящее только от принимаемых аргументов), не получится использовать .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'll be fine. Nobody else will need to multiply by 2, right?`);
saveToDB(i, n);
return n * 2;
};
const doubledArr = arr.map(double);

RIP, функциональная композиция 1958–2018

Забудьте о point-free композиции компонентов высшего порядка, которую вы использовали для инкапсуляции логики в разных частях приложения. Этот простой, декларативный синтаксис теперь вне закона:

const wrapEveryPage = compose(
withRedux,
withEnv,
withLoader,
withTheme,
withLayout,
withFeatures({ initialFeatures })
);

Будем импортировать все функции внутрь каждого компонента, или хуже — провалимся в запутанное, негибкое наследование, что справедливо признано анти-паттерном во всем цивилизованном мире, даже (особенно?) среди приверженцев объектно-ориентированного канона.

Прощайте, промисы. Прощайте, async/await

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

Откровенно говоря, не повредило бы выбросить все уроки по монадам и функторам. Они проще, чем мы приучили себя думать! Вот почему я обучаю Array.prototype.map и промисам прежде чем объясняю базовые концепции монад и функторов.

Вам понятны указанные методы? Если да, то вы на полпути к пониманию этих концепций.

Итак, чтобы избавиться от ФП

  • Откажитесь от большинства популярных JavaScript библиотек и фреймворков (они приведут вас к ФП!)
  • Не пишите чистые функции
  • Не используйте возможности языка: Math-функции (они чистые), иммутабельные методы строк и массивов ( .map(), .filter(), .forEach()), промисы и acync/await.
  • Больше безполезных классов
  • Удвойте (лучше — больше) количество кода за счет getter’ов и setter’ов буквально для всего.
  • Вооружившись “читаемым”, “явным” императивным подходом, решайте проблемы бизнес-логики вместе с рендером и I/O.

Скажите пока:

  • Отладке с перемещением во времени (Time travel debugging)
  • Безболезненной отмене уже реализованных фич/возврату к ним
  • Надежным и стабильным юнит-тестам
  • Свободному демо-тестированию и внедрению зависимостей (D/I)
  • Быстрому, независимому от сети юнит тестированию
  • Простым, удобным отладке и поддержке.

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

--

--