---
# System prepended metadata

title: 'Сдвиг парадигмы: переход от императивного программирования к функциональному'

---

# Сдвиг парадигмы: переход от императивного программирования к функциональному

На дворе вовсю идёт 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`.

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