# Сдвиг парадигмы: переход от императивного программирования к функциональному На дворе вовсю идёт 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`. Но обо всём об этом — как-нибудь в другой раз, ведь и так статья вышла немаленькая, и только на то, чтобы её переварить, потребуется какое-то время. Не говоря уже о том, что погружение в подход само по себе является ресурсоёмким.