# Сдвиг парадигмы: переход от императивного программирования к функциональному
На дворе вовсю идёт 2019 год, и мы с вами живём в эпоху расцвета пользовательских интерфейсов. Мобильные приложения давно глубоко вошли в нашу жизнь (даже я сейчас пишу эту статью со смартфона), а веб с каждым годом становится все более требовательным к отзывчивости и быстродействию, несмотря на значительно возросшие возможности современных компьютеров.
В самом деле, сейчас фронтенд-разработчиков выбирают не только по навыкам вёрстки и знаниям JavaScript и сопутствующих библиотек, но и по глубокому пониманию происходящих в ядре языка процессов и умению профилировать и оптимизировать код.
Это всё накладывается на заложенную в JS «безотказность»: неявное приведение типов, два nullable типа для разных случаев и отсутствие деления чисел на целые и дробные. Но такой код таит в себе большое количество подводных камней и неочевидных моментов, значительно усложняющих как процесс разработки так и отладку.
В итоге, код не спотыкается, казалось бы, на ровном месте, но ведёт себя куда более непредсказуемо. Слабая динамическая типизация даёт лишь кажущееся преимущество, а выскакивающие вместо явных ошибок в рантайме `undefined` неявно затыкают логические дыры, способные стоить компаниям тысячи человеко-часов в год, необходимые либо на явную обработку этих случаев, либо на отлов порождаемых ими ошибок в коде.
Давно пора было признать, что JavaScript — монополист в мире веба, и научиться играть по его правилам, а не пытаться интерпретировать их через призму куда более консервативных языков. Сами посудите, ни одна технология, соперничавшая с ним за место под солнцем, не смогла с ним конкурировать: ни старенький и немощный VBScript, ни более современный Dart, который компания Google ещё шесть лет назад проталкивала под флагом «убийцы JavaScript». Помимо них был целый зоопарк синтаксического сахара, который умер с выходом новой версии стандарта. Всё оказалось тщетно, и мы вынуждены жить с тем, что имеем.
Более того, JavaScript активно полез на сервер, в десктоп и мобильную разработку, где плотно занял свои ниши, при чём, вполне законно и обоснованно. Дошло до того, что можно вполне успешно писать на JavaScript в приложении, написанном на JavaScript (спасибо GitHub за Electron и Atom, пророка его). Так что, приди Dart на пять лет раньше, и этого, возможно, не случилось бы, но это время давно потеряно, и вряд ли когда-нибудь настанет более благоприятный момент для перехода. К тому же, слезать с иглы NPM уже слишком больно и дорого.
Так что же делать? Есть различные варианты. Конечно, всегда можно переползти на Dart, продираясь через дикие заросли и осваивая целину. Не самая удачная затея, требующая вложения большого количества ресурсов, но в России есть целая одна крупная компания, где он используется в качестве производственного стандарта. Если же NPM совсем не отпускает (что вероятнее), можно прикрутить на привычный синтаксис типизацию в виде TypeScript или Flow.
Но это всё — полумеры. Безусловно, типы важны, и о них даже стоит поговорить отдельно, но наличие статической типизации не спасает от всех проблем. Да, она не даёт ошибиться на этапе написания кода и больно бьёт по рукам при компиляции, но никак не освобождает нас от рантаймовых болячек из-за неправильного использования JavaScript.
Да, именно так: болячки — они не столько у языка, сколько образуются из-за неправильного его применения. То есть, проблема больше в подходе, чем в инструменте. А для того, чтобы её решить, подходить нужно комплексно, и начинать стоит именно с правил игры, ведь в нашем случае типы — это лишь флажки сапёра, а вот обезвреживание мин требует совсем других навыков и умений. В том числе, горячей головы, холодного сердца и прямых рук. Всё, как в программировании, только ошибки стоят дороже.
## Чистый JavaScript: инструкция для начинающего функционального сапёра
Очень многие языки, в принципе, позволяют себе вольности: написать можно что угодно, и, если оно синтаксически верно, а статический анализ кода не выявляет проблем с несоответствием типов, оно, скорее всего, скомпилируется и запустится без ошибок. Дальше всё упирается в рантайм.
И вот тут начинаются проблемы. Предположим, у нас есть функция, и мы вызываем её десять раз подряд, а результат каждого вызова выводим на экран:
```javascript=
let a = 0;
function inc() {
console.log(a);
a += 1;
}
inc(); // 0
inc(); // 1
inc(); // 2
inc(); // 3
inc(); // 4
inc(); // 5
inc(); // 6
inc(); // 7
inc(); // 8
inc(); // 9
```
Казалось бы, ничего особенного не произошло: каждый новый вызов выводит в консоль число, которое больше предыдущего на единицу. Но давайте завернём вызовы во что-то более похожее на пользовательский интерфейс:
```javascript=
document
.getElementById("increment")
.addEventListener("click", inc);
```
Теперь вызов функции происходит при нажатии на кнопку c `id="increment"`. Но что произойдёт, если между нажатиями на кнопку кто-то поменяет значение переменной `a`?
Да, выведется другой результат. Более того, язык позволяет нам заменить лежащее там число на всё, что угодно. И если в случае со строкой мы получим строку, на конце которой будет столько единиц, сколько раз мы нажмём на кнопку, то в случае с объектом или `undefined` мы получим значение `NaN` — волшебную вещь, которая получается при попытке применить арифметический оператор к значению, его не поддерживающему. Ещё одна логическая затычка.
То, с чем мы сейчас столкнулись, называется _побочным эффектом_ или _сайд-эффектом_. Функция использует внешнее состояние, к которому имеют доступ другие функции, и каждый последующий её вызов даёт разный результат — даже при учёте того, что мы передаём в неё одни и те же параметры. Более того, она способна сломаться. Нестабильно и непредсказуемо.
Давайте, исправим ситуацию. Для достижения этого мы можем пойти двумя путями. Первый — изолировать переменную и запретить к ней доступ. Мы можем получить желаемое, если сделаем следующее:
```javascript=
(function () {
let a = 0;
function inc() {
console.log(a);
return (a += 1);
}
document
.getElementById("increment")
.addEventListener("click", inc);
}());
```
Но так мы сразу теряем доступ к функции `inc`, и если захотим её переиспользовать, столкнёмся с другими побочными эффектами. Не совсем то, что нам нужно. Давайте, зайдём с другой стороны:
```javascript=
let a = 0;
function inc(a) {
console.log(a);
return a + 1;
}
a = inc(a);
```
Изменения относительно исходного примера, казалось бы, минимальные, но теперь наша функция не содержит побочных эффектов, то есть, её результат зависит только от переданных в неё параметров. Такие функции называют _чистыми_. Единственная проблема, с которой мы теперь столкнулись, вызов функции `inc` так просто не передать в качестве коллбэка на клик по кнопке.
Решается это достаточно просто:
```javascript=
const handleClick = () => {
a = inc(a);
};
document
.getElementById("increment")
.addEventListener("click", handleClick);
```
Правда, мы здесь имеем функцию с побочными эффектами, но вот уже теперь можем скрыть её внутри области видимости самовызывающейся функции. Давайте, посмотрим, что вышло в итоге:
```javascript=
function inc(a) {
if (typeof a === 'number') {
return a + 1;
}
}
(function () {
let a = 0;
const handleClick = () => {
console.log(a);
a = inc(a);
};
document
.getElementById("increment")
.addEventListener("click", handleClick);
}());
```
Вот здесь мы уже имеем полностью переиспользуемую чистую функцию `inc`, которая прибавляет к переданному числу единицу и возвращает результат, все побочные эффекты сконцентрированы в функции `handleClick`, а переменная `a` защищена от посягательств извне. Такой код становится куда более предсказуемым и стабильным. Можно его, конечно, дополнять и дополнять, добавить типизацию, чтобы в функцию `inc` нельзя было передать не число, но на данном этапе этого хватит.
Да, показанный пример в какой-то мере является вырожденным, ведь мы могли безболезненно затащить реализацию функции `inc` в `handleClick`. Поэтому, прежде, чем мы перейдём к более сложным примерам, я расскажу, почему я так не сделал.
## Функциональные джедаи против императивных штурмовиков
Начать стоит с того, что здесь произошёл тот самый переход от императивного стиля программирования к функциональному, о котором говорится в заголовке статьи. Дело в том, что JavaScript — гибридный язык, позволяющий писать в обоих стилях и свободно их смешивать.
Уже с самого начала я использовал особенность, свойственную именно функциональным языкам программирования: замыкание. Этот термин должен быть знаком всем, кто достаточно глубоко погружён в JavaScript. Суть его заключается в том, что после выполнения кода функции ссылки на объявленные внутри неё переменные и другие функции продолжают быть доступными внутри этих функций до тех пор, пока где-нибудь используется ссылка на хотя бы одну из них.
В нашем случае, `handleClick` — именно такая функция. Мы передали её в качестве обработчика события, и она хранится снаружи, поэтому мы можем вызвать её и безболезненно изменять переменную `a`, даже если последняя скрыта от прямого доступа — ровно до тех пор, пока не отцепим обработчик от события.
Это работает, в первую очередь, благодаря тому, как формируется область видимости функции. В JavaScript существует такое понятие, как _объект переменных_ или Variable Object, в котором поимённо хранятся все доступные переменные и функции. В глобальной области видимости в браузере это объект `window`.
Когда функция вызывается, её объект переменных формируется из объекта переменных внешней области, затем в него добавляются параметры, а после них — функции и переменные, объявленные внутри. Кстати, многие разработчики на собеседованиях ошибочно утверждают, что функция _ищет_ переменную, которую мы пытаемся взять, поднимаясь выше. На деле же выходит, что функция _уже_ знает про все доступные ей переменные.
Этот факт скромно намекает на то, что язык был заточен под функции с самого начала, а значит, мы можем вообще весь код завернуть в них. Почему бы и нет? Ведь мы живём так уже очень давно — и по сей день: на каком-то этапе развития фронтенда держать код каждого файла целиком внутри самовызывающейся функции стало производственной нормой. С появлением и распространением модульных систем, таких как RequireJS и CommonJS, это вышло на новый уровень: код заворачивался в функции без участия разработчика.
Современный производственный стандарт — система сборки Webpack — наследует и развивает этот принцип, позволяя не задумываться об изоляции вспомогательных переменных и функций. Наружу смотрят только артефакты, с которыми можно работать.
Правда, и тут есть свои сложности. Так, например, ссылочные типы экспортируются и импортируются по ссылкам, поэтому их потенциально можно изменять. Хорошим тоном здесь является предоставление наружу интерфейса, а не самого объекта. А ведь это снова функции.
Но ведь и в классических языках тоже можно всё обложить функциями, а современные версии некоторых из них уже даже предоставляют средства для создания замыканий. В чём же разница между ними и JavaScript?
А разница в том, что если императивный стиль — это про состояние приложения и его изменения по команде (императиву), то функциональный — про события, поведение и проекции (чистые функции, в нашем случае). И последнее больше походит на работу в условиях пользовательского ввода.
«Но ведь данные как-то надо хранить», — скажете вы. Надо. Но это не обязательно должно быть состояние приложения. Оно вполне может вычисляться при помощи комбинации и композиции проекций — на основе данных, изменения которых — события.
Таким образом, приложение превращается в живой организм, в котором каждую миллисекунду может что-то происходить, и каждое изменение приводит нас в конечное и равновесное состояние, которое хранится только в виде пользовательского интерфейса — допустимый для нас побочный эффект.
JavaScript позволяет делать это, что называется, «из коробки». Уже сейчас можно начать отказываться от императивного подхода и переходить к функциональному — вы ведь помните, что JS позволяет свободно комбинировать оба подхода, да?
Тем не менее, и у функциональной парадигмы есть ряд своих неудобств, большая часть которых вылезает в самом начале, но потом уже не встречается.
Во-первых, нужно научиться мыслить в парадигме и видеть открывающиеся возможности. Это не так просто, как может показаться. Переход займёт время и ресурсы, правда, потом принесёт немало бонусов.
Во-вторых, серьёзное функциональное программирование практически невозможно без освоения соответствующей теории. Она несложная, но также потребует времени и сильно зависит от бэкграунда: если вы не спали на парах по высшей математике и алгебре в университете (при условии, что они у вас вообще были), эта теория будет для вас достаточно простой, в противном же случае понадобится значительно больше времени.
Ну и, наконец, в-третьих, функциональный код может показаться более многословным, чем императивный, из-за чего может возникнуть иллюзия излишества: мол, писал раньше нормально, так зачем-то придумали всё это. Но мы вынуждены приносить жертвы в угоду стабильности и предсказуемости нашего кода — особенно, когда имеем дело с пользовательским интерфейсом.
Чтобы глубже понять то, о чём я только что сказал, рассмотрим ещё пару примеров.
## Проекции, или функциональное преобразование массивов
Когда речь заходит про более сложные типы данных, в отличие от рассмотренных выше примитивов, количество проблем увеличивается. Начиная с того, что в JavaScript они передаются по ссылке, и заканчивая тем, что до выхода современного стандарта автоматизация работы с ними превращалась в ад. Особенно, до 2011 года, пока в стандарте ECMAScript 5.1 не ввели расширения для стандартных структур данных.
Так, например, работа с массивами осуществлялась следующим образом:
```javascript=
var a = [1, 2, 3, 4, 5];
var b = [];
for (var i = 0; i < a.length; i++) {
b[i] = inc(a[i]);
}
```
Просто и понятно, но мы уже выяснили, что возвращать новое значение лучше, чем напрямую модифицировать существующее. Попробуем справиться с этим так же, как в предыдущем примере, и перепишем это, чтобы получить чистую функцию, которую можно переиспользовать:
```javascript=
function map(a, fn) {
var b = [];
if (a instanceof Array) {
for (var i = 0; i < a.length; i++) {
b[i] = fn(a[i]);
}
}
return b;
}
(function () {
var a = [1, 2, 3, 4, 5];
var b = map(a, inc); // [2, 3, 4, 5, 6]
var c = map(b, inc); // [3, 4, 5, 6, 7]
}());
```
Теперь мы можем быть уверены, что результат напрямую зависит только от входных данных. Пока мы передаём в функцию одни и те же значения, она будет возвращать одно и то же. Правда, как справедливо можно заметить, код стал ветвистее и немного сложнее.
Для того, чтобы справляться с этим, огромное сообщество JavaScript-разработчиков построило целую экосистему из библиотек. Так, например, наша функция `map`, за исключением некоторых деталей, [входит](https://github.com/lodash/lodash/blob/master/map.js) в пакет Lodash.
Точно такой же результат даст нам вызов на массиве метода `map`, появившегося в JavaScript 1.6:
```javascript=
var b = a.map(inc); // [2, 3, 4, 5, 6]
```
В той же версии у `Array` появился и ряд других методов, каждый из которых может преобразовывать массив во что-то другое. Так, например, мы можем сделать что-то подобное:
```javascript=
function isEven(x) {
return x % 2 === 0;
}
function sum(a, b) {
return a + b;
}
(function () {
var a = [1, 2, 3, 4, 5];
var b = a
.map(inc) // [2, 3, 4, 5, 6]
.filter(isEven) // [2, 4, 6]
.reduce(sum, 0); // 12
}());
```
Таким образом, мы получили возможность управлять массивом, передавая в его методы простые чистые функции. Но я предлагаю рассмотреть более интересный пример.
Задача: считать количество кликов по кнопке, выводить сообщение, когда оно достигает 10, и начинать счёт заново.
Руководствуясь предыдущими рецептами, мы можем написать что-то такое:
```javascript=
(function () {
let a = 0;
const handleClick = () => {
a = inc(a);
if (a === 10) {
console.log('10 clicks!');
a = 0;
}
};
document
.getElementById('increment')
.addEventListener('click', handleClick);
}());
```
И оно будет прекрасно работать. Правда, мы всё ещё держим для этого переменную. А если нам нужно реагировать на одни и те же события разным образом? Конечно, мы всегда можем добавить ещё одну функцию, добавить её в качестве обработчика и спокойно жить. Но мы можем не повторять код и поступить более элегантно. Нам нужно лишь воспользоваться шаблоном _«Издатель-подписчик»_, также более известным под названием _Observable-Observer_.
## Функциональный наблюдатель
Давайте попробуем реализовать простейший `Observable`. Нам понадобится определить метод `subscribe`, которому передадим объект, который будет следить за событиями, а для того, чтобы не приколачивать гвоздями их обработку, сделаем возможность передавать в конструкторе функцию, которая будет вызвана во время подписки. Выглядеть это будет примерно так:
```javascript=
class Observable {
_subscribe = () => {};
constructor(subscribe) { // function
if (subscribe) {
this._subscribe = subscribe;
}
}
subscribe(subscriber) {
this._subscribe(subscriber);
}
}
```
Объект `subscriber` у нас пока будет обладать лишь функцией `next`, которая будет вызываться после каждого события, и полем `value`, в котором мы будем держать счётчик. Его мы будем передавать при подписке.
```javascript=
const nObserver = {
value: 0,
next() {
this.value = inc(this.value);
if (this.value === 10) {
console.log('10 clicks!');
this.value = 0;
}
},
};
```
И, наконец, нам нужно создать `Observable` и подписаться на него:
```javascript=
const nObservable = new Observable(subscriber => {
document
.getElementById('increment')
.addEventListener('click', e => {
subscriber.next(e);
});
});
nObservable.subscribe(nObserver);
```
Если добавить в функцию `next` вывод в консоль на каждый клик, мы получим практически то же самое, что и в самом первом примере, только теперь мы не должны повторять код. Нам достаточно создать объект подписчика и подписаться на `nObservable` с другим обработчиком, который, например, может раскрашивать кнопку в случайный цвет, а подписаться через две секунды после подписки первого:
```javascript=
function normalizedRandom(a, b) {
return Math.floor(Math.random() * (b - a)) + a;
}
function channel() {
return normalizedRandom(0, 255);
}
const nObserver2 = {
next(e) {
e.target.style.background = `rgb(${channel()}, ${channel()}, ${channel()})`;
},
};
setTimeout(() => {
nObservable.subscribe(nObserver2);
}, 2000);
```
Теперь, если мы будем нажимать на нашу кнопку, на каждое нажатие кнопка будет менять цвет (по прошествии двух секунд), а на каждое десятое в консоль будет выводиться соответствующее сообщение. Казалось бы, зачем такое усложнение, но если приглядеться внимательнее, наш код может превратиться во что-то такое:
```javascript=
function normalizedRandom(a, b) {
return Math.floor(Math.random() * (b - a)) + a;
}
function channel() {
return normalizedRandom(0, 255);
}
const nObservable = new Observable(subscriber => {
document
.getElementById('increment')
.addEventListener('click', e => {
subscriber.next(e);
});
});
nObservable.subscribe({
value: 0,
next() {
this.value = inc(this.value);
if (this.value === 10) {
console.log('10 clicks!');
this.value = 0;
}
},
});
setTimeout(() => {
nObservable.subscribe({
next(e) {
e.target.style.background = `rgb(${channel()}, ${channel()}, ${channel()})`;
},
});
}, 2000);
```
Получилось более чем чисто, если не считать работы с DOM-элементами, но я выше упоминал, что изменения состояния интерфейса — вполне приемлемый побочный эффект. Более того, объект подписчика, в котором мы держим и значение, и обработчик, невозможно ни изменить, ни прочитать: он намертво скрыт внутри экземпляра класса `Observable` как часть замыкания, и явно нигде его значение не записывается.
Что ещё может предложить этот шаблон? Много чего. Обработку практически любых синхронных и асинхронных событий, преобразования, слияния двух потоков событий и т.п. — об этом подходе вполне можно написать отдельную статью. Одна же из наиболее полных его реализаций — библиотека RxJS.
Помимо неё существуют библиотеки и для других языков: от C\++ до Swift и Kotlin. Более подробно можно посмотреть в [GitHub проекта ReactiveX](https://github.com/ReactiveX). Их обилие прекрасно показывает, что практически любой язык позволяет перейти от императивного подхода к функциональному при правильном его использовании.
## Императивное заключение функциональной статьи
На этом — всё. Или же нет? Ведь мы рассмотрели далеко не все аспекты, касающиеся функционального подхода — лишь пробежались по верхам. Или, если говорить точнее, наоборот, зацепили лишь самые основы: научились строить вычисления чистыми функциями, подписываться на события так, чтобы их обработчики порождали как можно меньше побочных эффектов… Но самое главное, поняли, что код становится значительно более эффективным, если оформляется в функциональном стиле, несмотря на трудозатраты в самом начале.
Учитывая, что мы живём в эпоху расцвета пользовательских интерфейсов, игнорировать современные актуальные подходы к решению задач невозможно. Мы вынуждены жертвовать простотой кода в угоду доступности и стабильности интерфейса. Поэтому вопрос перехода от императивного подхода к функциональному становится во главу угла: написание кода в этой парадигме делает его более предсказуемым и однозначным. Тем не менее, то, что мы затронули — лишь вершина айсберга.
Как уже было сказано, отдельно стоит затронуть как типизацию данных в JavaScript, так и работу паттерна Observable-Observer. Более глубокое погружение в функциональное программирование может дать нам ещё больше преимуществ. Так, например, заимствование подхода языка Haskell поможет нам решить проблему с `null` и `undefined`, а также, позволит обрабатывать ошибки без использования блока `try {} catch`.
Но обо всём об этом — как-нибудь в другой раз, ведь и так статья вышла немаленькая, и только на то, чтобы её переварить, потребуется какое-то время. Не говоря уже о том, что погружение в подход само по себе является ресурсоёмким.