Skip to content

Latest commit

 

History

History

glossary_of_modern_javaScript_concepts_part_1

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Справочник современных концепций JavaScript: часть 1

Основы функционального программирования, реактивного программирования и функционального реактивного программирования на JavaScript.

Кратко: В первой части серии «Справочник современных концепций JavaScript» мы познакомимся с функциональным программированием (FP), реактивным программированием (RP) и функциональным реактивным программированием (FRP). Для этого мы узнаем о чистоте, состоянии и его отсутствии, неизменяемости и изменяемости, императивном и декларативном программировании, функциях высшего порядка, observables и парадигмах FP, RP, FRP.

Введение

За последние годы JavaScript сильно развился и не думает на этом останавливаться. Однако множество концепций, рождающихся в JS-блогах и документациях, так и остаются невостребованными широким кругом разработчиков интерфейсов. В настоящей серии статей мы изучим концепции, требующие как среднего, так и продвинутого уровней подготовки, а также рассмотрим, как они вписываются в современный JavaScript.

Концепции

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

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

Чистота

Чистые функции

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

function half(x) {
    return x / 2;
}

Функция half(x) принимает число x и возвращает значение половины x. Если мы передадим этой функции аргумент 8, она вернет 4. После вызова чистая функция всегда может быть заменена результатом своей работы. Например, мы могли бы заменить half(8) на 4: где бы эта функция не использовалась в нашем коде, подмена никак не повлияла бы на конечный результат. Это называется ссылочной прозрачностью.

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

var someNum = 8;

// это НЕ чистая функция
function impureHalf() {
  return someNum / 2;
}

Итого:

  • Чистые функции должны принимать аргументы.
  • Одни и те же входные данные (аргументы) всегда произведут одинаковые выходные данные (вернут одинаковый результат).
  • Чистые функции основываются только на внутреннем состоянии и не изменяют внешнее (примечание: console.log изменяет глобальное состояние).
  • Чистые функции не производят побочных эффектов.
  • Чистые функции не могут вызывать нечистые функции.

Нечистые функции

Нечистая функция изменяет состояние вне своей области видимости. Любые функции с побочными эффектами (см. далее) - нечистые, ровно как и процедурные функции без возвращаемого значения.

Рассмотрим следующие примеры:

// нечистая функция производит побочный эффект
function showAlert() {
  alert('This is a side effect!');
}

// нечистая функция изменяет внешнее состояние
var globalVal = 1;
function incrementGlobalVal(x) {
  globalVal += x;
}

// нечистая функция процедурно вызывает чистую функцию
function proceduralFn() {
  const result1 = pureFnFirst(1);
  const result2 = pureFnLast(2);
  console.log(`Done with ${result1} and ${result2}!`);
}

// нечистая функция выглядит чистой,
// но возвращает разные значения
// при одинаковых входных данных
function getRandomRange(min, max) {
  return Math.random() * (max - min) + min;
}

Побочные эффекты в JavaScript

Когда функция или выражение изменяет состояние вне своего контекста, результат является побочным эффектом. Примеры побочных эффектов: вызов API, манипулирование DOM, вывод alert, запись в базу данных и так далее. Если функция производит побочные эффекты, она считается нечистой. Функции, вызывающие побочные эффекты, менее предсказуемы и их труднее тестировать, поскольку они приводят к изменениям вне их локальной области видимости.

Подводя итог: чистота

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

Дополнительную информацию о чистоте можно найти на следующих ресурсах:

Состояние

Состояние - информация, к которой программа имеет доступ и с которой может работать в определенный момент времени. Сюда входят данные, хранящиеся в памяти, порты ввода/вывода, базы данных и так далее. Например, содержимое переменных в приложении в любой данный момент времени репрезентативно для состояния приложения.

С состоянием

Программы, приложения или компоненты с состоянием хранят в памяти данные о текущем состоянии. Они могут изменять состояние, а также имеют доступ к его истории. Следующий пример демонстрирует это:

var number = 1;
function increment() {
  return number++;
}
increment(); // глобальная переменная изменяется: number = 2

Без состояния

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

var number = 1;
function increment(n) {
  return n + 1;
}
increment(number); // глобальная переменная НЕ изменяется: возвращает 2

Приложения без состояния все ещё управляют состоянием. Однако они возвращают своё текущее состояние без изменения предыдущего состояния. Это принцип функционального программирования.

Подводя итог: состояние

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

Дополнительную информацию о состоянии можно найти на следующих ресурсах:

Неизменяемость и изменяемость

Концепции неизменяемости и изменяемости более туманны в JavaScript, чем в некоторых других языках программирования. Тем не менее, вы много услышите о неизменяемости при чтении о функциональном программировании в JS. Важно знать, что эти термины означают в классическом понимании, и как они реализуются в JavaScript. Определения достаточно просты:

Неизменяемый

Если объект является неизменяемым, его значение не может быть изменено после создания.

Изменяемый

Если объект изменяем, его значение может быть изменено после создания.

Реализация: неизменяемость и изменяемость в JavaScript

В JavaScript строки и числовые литералы реализованы неизменяемыми. Это легко понять, если рассмотреть, как мы работаем с ними:

var str = 'Hello!';
var anotherStr = str.substring(2);
// результат: str = 'Hello!' (не изменена)
// результат: anotherStr = 'llo!' (новая строка)

Используя метод .substring() на нашем Hello!, строка не изменяет исходную строку. Вместо этого она создает новую строку. Мы могли бы переопределить значение переменной str на что-то другое, но, как только мы создали нашу строку Hello!, она навсегда останется Hello!.

Числовые литералы также неизменяемы. Следующий пример всегда будет иметь одинаковый результат:

var three = 1 + 2;
// результат: three = 3

Ни при каких обстоятельствах 1 + 2 не может стать чем-либо, кроме 3.

Это демонстрирует, что в JavaScript присутствует реализация неизменяемости. Однако разработчики JS знают, что язык позволяет изменить многое. Например, объекты и массивы изменяемы. Рассмотрим следующий пример:

var arr = [1, 2, 3];
arr.push(4);
// результат: arr = [1, 2, 3, 4]

var obj = { greeting: 'Hello' };
obj.name = 'Jon';
// результат: obj = { greeting: 'Hello', name: 'Jon' }

В этих примерах исходные объекты изменены. Новые объекты не возвращаются.

Чтобы узнать больше об изменяемости в других языках, ознакомьтесь с Изменяемые и неизменяемые объекты.

На практике: изменяемость в JavaScript

Функциональное программирование в JavaScript хорошо развивается. Но по своей сущности JS - очень изменчивый язык, состоящий из множества парадигм. Ключевая особенность функционального программирования - неизменяемость. Другие функциональные языки выбросят ошибку, когда разработчик попытается изменить неизменяемый объект. Тогда как мы можем примирить врожденную изменяемость JS при написании функционального или функционального реактивного JS?

Когда мы говорим о функциональном программировании в JS, слово «неизменяемое» используется много, но разработчик обязан всегда держать её в голове. Например, Redux полагается на одно неизменяемое дерево состояний. Однако сам JavaScript способен изменять объект состояния. Чтобы реализовать неизменяемое дерево состояний, нам нужно каждый раз при изменении состояния возвращать новый объект состояния.

Для неизменяемости объекты JavaScript также могут быть заморожены с помощью Object.freeze(obj). Обратите внимание, что это "неглубокая" заморозка - значения объектов внутри замороженного объекта все ещё могут быть изменены. Для гарантированной неизменяемости такие функции "глубокой" заморозки, как Mozilla deepFreeze() и npm deep-freeze могут рекурсивно замораживать объекты. Замораживание наиболее применимо в тестах, а не в приложении. Тесты будут оповещать разработчиков о возникновении изменений, чтобы их можно было исправить, и избежать загромождающего Object.freeze в основном коде.

Существуют также библиотеки, поддерживающие неизменяемость в JS. Mori предоставляет постоянные структуры данных на основе Clojure. Immutable.js от Facebook также предоставляет неизменяемые коллекции для JS. Библиотеки утилит, такие как Underscore.js и lodash, предоставляют методы и модули для более функционального стиля программирования (а стало быть направленного на неизменяемость).

Подводя итог: неизменяемость и изменяемость

В целом, JavaScript - язык с сильной изменяемостью. Некоторые стили JS-кодирования опираются на эту врожденную изменяемость. Однако, при написании функционального JS, реализация неизменяемости требует внимательности. Если вы что-то нечаянно модифицируете, JS не будет выбрасывать ошибки. Тестирование и библиотеки могут помочь, но работа с неизменяемостью в JS требует практики и методологии.

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

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

Дополнительную информацию о неизменяемости и изменяемости можно найти на следующих ресурсах:

Императивное и декларативное программирование

Хотя некоторые языки были разработаны как императивные (C, PHP) или декларативные (SQL, HTML), JavaScript и другие (такие как Java и C# могут поддерживать обе парадигмы программирования.

Большинство разработчиков, знакомых с даже самым простым JavaScript, писали императивный код: инструкции, информирующие компьютер как достичь желаемого результата. Если вы использовали цикл for, вы писали императивный JS.

Декларативный код сообщает компьютеру, что вы хотите достичь, а не как, и компьютер сам заботится о том, как достичь конечного результата без явного описания этого разработчиком. Если вы использовали Array.map, вы писали декларативный JS.

Императивное программирование

Императивное программирование описывает логику работы программы в явных командах с операторами, изменяющими состояние программы.

Рассмотрим функцию, увеличивающую каждое число в массиве целых чисел. Императивным примером в JavaScript может быть:

function incrementArray(arr) {
  let resultArr = [];
  for (let i = 0; i < arr.length; i++) {
    resultArr.push(arr[i] + 1);
  }
  return resultArr;
}

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

Декларативное программирование

Декларативное программирование описывает, что выполняет логика программы без описания того, как это сделать.

Очень простой пример декларативного программирования может быть продемонстрирован с помощью SQL. Мы можем запросить таблицу базы данных (People) для людей с фамилией Smith следующим образом:

SELECT * FROM People WHERE LastName = 'Smith'

Этот код легко читается и описывает то, что мы хотим достичь. Нет описания того, как результат должен быть достигнут. Компьютер об этом позаботится сам.

Теперь рассмотрим функцию incrementArray(), которую мы императивно реализовали выше. Давайте сейчас реализуем это декларативно:

function incrementArray(arr) {
  return arr.map(item => item + 1);
}

Мы показываем, что хотим достичь, но не как это работает. Метод Array.map() возвращает новый массив с результатами выполнения обратного вызова для каждого элемента из переданного массива. Этот подход не изменяет существующие значения и не включает в себя последовательную логику, раскрывающую, как он создает новый массив.

Примечание: в JavaScript map, reduce и filter - декларативные, функциональные методы массивов. Библиотеки утилит, такие как lodash, предоставляют такие методы takeWhile, uniq, zip и другие в дополнение к map, reduce и filter.

Подводя итог: императивное и декларативное программирование

JavaScript допускает как парадигмы императивного, так и декларативного программирования. Большая часть кода JS, который мы читаем и пишем, императивная. Однако, с ростом популярности функционального программирования в JS, декларативные подходы распространяются все больше.

Декларативное программирование имеет очевидные преимущества в отношении краткости и читаемости, но в тоже время может походить на магию. Многие новички в JavaScript могут на базе накопленного опыта писать императивный JS перед глубоким погружением в декларативное программирование.

Дополнительную информацию об императивном и декларативном программировании можно найти на следующих ресурсах:

Функции высшего порядка

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

В JavaScript функции являются объектами первого класса. Они могут храниться и передаваться как значения: мы можем присвоить функцию переменной или передать функцию другой функции.

const double = function(x) {
  return x * 2;
}
const timesTwo = double;

timesTwo(4); // результат: возвращает 8

Обратный вызов - один из примеров использования функции в качестве аргумента. Обратные вызовы могут быть встроенными анонимными функциями или именованными функциями:

const myBtn = document.getElementById('myButton');

// анонимная функция обратного вызова
myBtn.addEventListener('click', function(e) { console.log(`Событие клика: ${e}`); });

// именованная функция обратного вызова
function btnHandler(e) {
  console.log(`Событие клика: ${e}`);
}
myBtn.addEventListener('click', btnHandler);

Мы также можем передать функцию в качестве аргумента любой другой созданной нами функции, а затем выполнить этот аргумент:

function sayHi() {
  alert('Hi!');
}
function greet(greeting) {
  greeting();
}
greet(sayHi); // "Hi!"

Примечание: при передаче именованной функции в качестве аргумента, как и в двух приведенных выше примерах, мы не используем круглые скобки (). Таким образом мы передаём функцию как объект. Круглые скобки выполняют функцию и передают результат вместо самой функции.

Функции высшего порядка также могут возвращать другую функцию:

function whenMeetingJohn() {
  return function() {
    alert('Hi!');
  }
}
var atLunchToday = whenMeetingJohn();

atLunchToday(); // "Hi!"

Подводя итог: функции высшего порядка

Природа JavaScript функций как объектов первого класса делает их основой функционального программирования в JS.

Дополнительную информацию о функциях высшего порядка можно найти на следующих ресурсах:

Функциональное программирование

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

На практике: функциональное программирование на JavaScript

Функциональное программирование охватывает приведенные выше концепции следующими способами:

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

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

Давайте рассмотрим пример. Скажем, у нас есть текст, и мы хотим посчитать количество слов в нем. Мы также хотим найти ключевые слова длиной более пяти символов. Используя функциональное программирование, наш результирующий код может выглядеть примерно так:

const fpCopy = `Functional programming is powerful and enjoyable to write. It's very cool!`;

// убирает из строки пунктуацию
const stripPunctuation = (str) =>
  str.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '');

// разбивает строку по пробелам, чтобы создать массив
const getArr = (str) =>
  str.split(' ');

// подсчитывает количество элементов массива
const getWordCount = (arr) =>
  arr.length;

// находит элементы массива длиннее 5 символов
// и приводит их к нижнему регистру
const getKeywords = (arr) =>
  arr
    .filter(item => item.length > 5)
    .map(item => item.toLowerCase());

// обрабатывает текст: подготавливает строки, создает массив, подсчитывает слова и получает ключевые слова
function processCopy(str, prepFn, arrFn, countFn, kwFn) {
  const copyArray = arrFn(prepFn(str));

  console.log(`Word count: ${countFn(copyArray)}`);
  console.log(`Keywords: ${kwFn(copyArray)}`);
}

processCopy(fpCopy, stripPunctuation, getArr, getWordCount, getKeywords);
// результат: число слов: 11
// результат: ключевые слова: functional,programming,powerful,enjoyable

Код доступен для запуска здесь - JSFiddle: Functional Programming with JavaScript. Он разбит на понятные, декларативные функции с четким назначением. Если мы пройдем по нему и прочитаем комментарии, то никаких дополнительных разъяснений кода не потребуется. Каждая функция ядра - модульная и зависит только от её входных данных (чистая). Последняя функция обрабатывает ядро ​​для генерации общих выходных данных. Функция processCopy() - нечистый контейнер, выполняющий ядро ​​и управляющий побочными эффектами. Мы использовали функцию высшего порядка, принимающую другие функции в качестве аргументов для поддержания функционального стиля.

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

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

Дополнительную информацию о функциональном программировании можно найти на следующих ресурсах:

Observables

Observables похожи на массивы, за исключением того, что элементы не хранятся в памяти, а поступают асинхронно с течением времени (потоки). Мы можем подписаться на observables и реагировать на их события. JavaScript observables - это реализация шаблона observer. Реактивные расширения (обычно называемые с префиксом Rx) предоставляют observables для JS через RxJS.

Для демонстрации концепции observables давайте рассмотрим простой пример: изменение размера окна браузера. В этом контексте observables максимально понятны. Изменение размера окна браузера испускает поток событий в течение определенного периода времени (пока окно принимает нужный размер). Мы можем создать observable и подписаться на него, чтобы реагировать на поток событий изменения размера:

// создаём поток изменения размера окна
// с задержкой 350 миллисекунд
const resize$ =
  Rx.Observable
    .fromEvent(window, 'resize')
    .throttleTime(350);

// подписываемся на observable resize$
// и логируем ширину и высоту окна
const subscription =
  resize$.subscribe((event) => {
    let t = event.target;
    console.log(`${t.innerWidth}px x ${t.innerHeight}px`);
  });

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

Горячие observable

Пользовательские интерфейсные события (нажатие кнопки, перемещения мыши и т.д.) - горячие. Горячие observable всегда будут срабатывать, даже если мы специально не реагируем на них подпиской. Пример изменения размера окна выше - горячий observable: resize$ observable отрабатывает вне зависимости от того, существует подписка или нет.

Холодные observable

Холодные observable срабатывают только тогда, когда мы подписываемся на него. Если мы подпишимся снова, он начнёт все сначала.

Давайте создадим observable массив чисел от 1 до 5:

// создаём поток чисел
const source$ = Rx.Observable.range(1, 5);

// подписываемся на source$ observable
const subscription = source$.subscribe(
  (value) => { console.log(`Next: ${value}`); }, // onNext
  (event) => { console.log(`Error: ${event}`); }, // onError
  () => { console.log('Completed!'); }  // onCompleted
);

Мы можем подписаться на только что созданный source$ observable. После подписки, значения последовательно посылаются к observer. Обратный вызов onNext логирует значения: Next: 1, Next: 2, и т.д. до завершён.я: Completed!. Созданный нами холодный observable source$ не отрабатывает до того, пока мы не подпишемся на него.

Подводя итог: observables

Observables являются потоками. Мы можем наблюдать любой поток: от событий изменения размеров в существующих массивах до ответов API. Мы можем создать observables практически из всего, что угодно. Promise - тот же observable, но отдающий только одно значение, а observables могут возвращать много значений с течением времени.

Мы можем работать с observables по-разному. RxJS использует множество операторов. Observables часто визуализируется с помощью точек на линии, как показано на сайте RxMarbles. Поскольку поток состоит из асинхронных событий с течением времени, легко осмыслять это линейным способом и использовать именно такие зрительные образы, чтобы понять реактивные операторы. Например, следующие изображение от RxMarbles иллюстрирует оператор фильтра:

Дополнительную информацию об observables можно найти на следующих ресурсах:

Реактивное программирование

Реактивное программирование связано с декларативным (что делать, а не как) наблюдением и реагированием на поступающие события во времени.

Реактивное программирование часто связано с Reactive Extensions, API для асинхронного программирования с observable потоками. Реактивные расширения (с префиксом Rx) предоставляют библиотеки для различных языков, включая JavaScript (RxJS).

На практике: реактивное программирование на JavaScript

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

<!-- HTML -->
<input id="confirmation-code" type="text">
<p>
  <strong>Valid code attempt:</strong>
  <code id="attempted-code"></code>
</p>

Мы будем использовать RxJS и для реализации нашей функциональности создадим поток входных событий, вот так:

// JS
const confCodeInput = document.getElementById('confirmation-code');
const attemptedCode = document.getElementById('attempted-code');

const confCodes$ =
  Rx.Observable
    .fromEvent(confCodeInput, 'input')
    .map(e => e.target.value)
    .filter(code => code.length === 6);

const subscription = confCodes$.subscribe(
  (value) => attemptedCode.innerText = value,
  (event) => { console.warn(`Error: ${event}`); },
  () => { console.info('Completed!'); }
);

Этот код можно запустить на JSFiddle: Reactive Programming with JavaScript. Мы наблюдаем события из элемента confCodeInput. Затем мы используем оператор mapдля получения значения из каждого входного события. Далее мы фильтруем любые результаты, не являющиеся шестью символами, чтобы они не появлялись в возвращенном потоке. Наконец, мы подписываемся на наш confCodes$ observable и выводим подошедший вариант. Обратите внимание, что это произошло в ответ на событие, декларативно, - это суть реактивного программирования.

Подводя итог: реактивное программирование

Парадигма реактивного программирования включает наблюдение и реагирование на события в асинхронных потоках данных. RxJS используется в Angular и набирает популярность как решение JavaScript для реактивного программирования.

Дополнительную информацию о реактивном программировании можно найти на следующих ресурсах:

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

Говоря простыми словами, функциональное реактивное программирование можно свести к декларативному реагированию на события или поведение с течением времени. Чтобы понять принципы FRP более подробно, давайте взглянем на формулировку FRP. Затем мы рассмотрим его использование применительно к JavaScript.

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

Более полное определение от родоначальника FRP Конала Эллиота звучит так: функциональное реактивное программирование - денотативное и продолжительное во времени. Эллиот предпочитает описывать эту парадигму программирования как денотативное программирование с продолжительностью во времени, а не «функциональное реактивное программирование».

Функциональное реактивное программирование, в его самом простом, оригинальном определении, имеет два фундаментальных свойства:

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

И ещё раз: функциональное реактивное программирование - это программирование в декларативном стиле с изменяющимися во времени значениями.

Чтобы понять продолжительность во времени, рассмотрим аналогию с использованием векторной графики. Векторная графика имеет бесконечное разрешение. В отличие от растровой графики (дискретное разрешение) векторная графика масштабируется безгранично, она никогда не перерисовывается или становится нечеткой. «Выражения FRP описывают целые эволюции значений во времени, представляя эти эволюции непосредственно как значения первого класса», - Конал Эллиот.

Функциональное реактивное программирование должно быть:

  • динамическим: может реагировать во времени или на изменение входных данных
  • изменяющимся во времени: реактивное поведение может изменяться с продолжительностью во времени, а реактивные значения изменяются дискретно
  • эффективным: минимизировать объем обработки, необходимый при изменении входных данных
  • осведомленным о прошлом: чистые функции сопоставляют предыдущее значение состояния со следующим; изменения состояния относятся к локальному элементу, а не к глобальному состоянию программы

Здесь можно рассмотреть слайды Колала Эллиота о сущности и происхождении FRP. Язык программирования Haskell поддается истинному FRP благодаря своей функциональной, чистой и ленивой природе. Эван Кзаплиски, создатель Elm, даёт большой обзор FRP в своём выступлении «Управление временем и пространством: понимание многих подходов FRP».

В самом деле, давайте коротко поговорим об Elm Эвана Кзаплиски. Elm - это функциональный, типизированный язык для создания веб-приложений. Он компилируется в JavaScript, CSS и HTML. Архитектура Elm послужила вдохновением для контейнера состояния Redux приложений JS. Первоначально Elm позиционировался истинным функционально реактивным языком программирования, но начиная с версии 0.17 он реализовывал подписки вместо сигналов в интересах облегчения изучения и использования языка. На этом Elm простился с FRP.

На практике: функциональное реактивное программирование на JavaScript

Традиционное определение FRP может быть трудным для понимания, особенно для разработчиков, не имеющих опыта работы с такими языками, как Haskell или Elm. Однако этот термин чаще всего появляется в интерфейсной экосистеме, поэтому давайте проясним его применение в JavaScript.

Для согласования всего, что вы, возможно, читали о FRP в JS, важно понять, что Rx, Bacon.js, Angular и другие не согласуются с двумя основными принципами определения FRP Конала Эллиота. Эллиот заявляет, что Rx и Bacon.js не являются FRP. Вместо этого они «композиционные системы событий, вдохновленные FRP».

Функциональное реактивное программирование в своей реализации в JavaScript относится к программированию в функциональном стиле при создании и реагировании на потоки. Это довольно далеко от оригинальной формулировки Эллиота (которая специально исключает потоки как компонент), но тем не менее вдохновляется традиционными FRP.

Также очень важно понять, что JavaScript по сути взаимодействует с пользователем и пользовательским интерфейсом, DOM и часто с базой данных. Побочные эффекты и императивный код де факто являются для него стандартом, даже при использовании функционального или функционального реактивного подхода. Без императивного или нечистого кода веб-приложение JS с пользовательским интерфейсом не было бы очень полезным, поскольку оно не могло бы взаимодействовать со своей средой.

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

// создает observable времени, добавляющий каждую 1 секунду элемент,
// и добавляет к итоговую потоку значения события
const time$ =
  Rx.Observable
    .timer(0, 1000)
    .timeInterval()
    .map(e => e.value);

// создает observable движения мыши
// с задержкой 350 миллисекунд
// и добавляет к итоговую потоку объекты с x и y координатами
const move$ =
  Rx.Observable
    .fromEvent(document, 'mousemove')
    .throttleTime(350)
    .map(e => { return {x: e.clientX, y: e.clientY} });

// объединяет потоки observable времени и observable движения мыши
// и заканчивает их через 10 секунд
const source$ =
  Rx.Observable
    .merge(time$, move$)
    .takeUntil(Rx.Observable.timer(10000));

// подписывается на объединенный source$ observable
// если значение число, то createTimeset()
// если значение объект координат, то addPoint()
const subscription =
  source$.subscribe(
    // обработчик события
    (x) => {
      if (typeof x === 'number') {
        createTimeset(x);
      } else {
        addPoint(x);
      }
    },
    // обработчик ошибки
    (err) => { console.warn('Error:', err); },
    // обработчик завершён.я
    () => { console.info('Completed'); }
  );

// добавляет элемент в DOM для вывода точек, затронутых в конкретную секунду
function createTimeset(n) {
  const elem = document.createElement('div');
  const num = n + 1;
  elem.id = 't' + num;
  elem.innerHTML = `<strong>${num}</strong>: `;
  document.body.appendChild(elem);
}

// добавляет точки, затронутые в конкретную секунду
function addPoint(pointObj) {
  // добавляет точки к последнему добавленному элементу
  const numberElem = document.getElementsByTagName('body')[0].lastChild;
  numberElem.innerHTML += ` (${pointObj.x}, ${pointObj.y}) `;
}

Вы можете проверить этот код в действии в JSFiddle: FRP-вдохновленном JavaScript. Запустите скрипт и, пока идёт подсчет до 10, наведите указатель мыши в экран с результатом. Вы должны увидеть координаты мыши вместе со счетчиком. Тогда на экран выведется, где была ваша мышь во время каждого 1-секундного интервала времени.

Давайте кратко обсудим эту реализацию шаг за шагом.

Сначала мы создаём observable time$. Это таймер, добавляющий значение в коллекцию каждые 1000 миллисекунд (каждую секунду). Нам нужно обработать событие таймера, чтобы извлечь его значение и добавить к результирующему потоку.

Затем мы создаём move$ observable из события document.mousemove. Движение мыши продолжительно во времени. В любой точке последовательности существует бесконечное количество точек между ними. Мы ограничиваем эту бесконечность, так что результирующий поток становится более управляемым. Затем мы обрабатываем событие и возвращаем объект с значениями x и y, чтобы сохранить координаты мыши.

Затем мы хотим объединить потоки time$ и move$. Для этого используем оператор объединения. Так мы можем определить, какие движения мыши произошли в течение каждого интервала времени. Мы будем называть результирующий observable source$. Мы также ограничим observable source$ так, чтобы он завершился через десять секунд (10000 миллисекунд).

Теперь, когда у нас есть объединенный поток времени и движения, мы создаём подписку на observable source$, чтобы мы могли реагировать на него. В обратном вызове onNext мы проверяем, является ли значение числом или нет. Если это так, мы вызываем функцию createTimeset(). Если это объект координат, вызываем addPoint(). В обратных вызовах onError и onCompleted мы просто логируем информацию.

Давайте рассмотрим функцию createTimeset(n). Мы создаём новый элемент div для каждого второго интервала, помечаем его и добавляем в DOM.

В функции addPoint(pointObj) мы выводим в div последние координаты в последнем временном интервале. Это связывает каждый набор координат с соответствующим временным интервалом. Теперь мы можем увидеть, где мышь находилась в конкретный момент времени.

Примечание: эти функции нечистые: у них нет возвращаемого значения и они производят побочные эффекты. Побочные эффекты - манипуляции DOM. Как упоминалось ранее, JavaScript, который мы пишем для наших приложений, часто взаимодействует с областью видимости вне его функций.

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

FRP представляет собой написание действий, которые, используя чистые функции, реагируют на события и переводят состояние с предыдущего момента времени к следующему. FRP в реализации JavaScript не придерживается двух основных принципов FRP Конала Эллиота, но в абстрагировании от оригинальной концепции есть определенный смысл. JavaScript сильно зависит от побочных эффектов и императивного программирования, но мы, безусловно, можем использовать преимущества концепций FRP для улучшения нашего JS.

Наконец, рассмотрим эту цитату из первого издания Eloquent JavaScript: «Fu-Tzu написал небольшую программу, использующую глобальное состояние и сомнительные переплетения, и, прочитав её, студент спросил:« Вы предупреждали нас против этих методов, но я нахожу их в вашей программе. Как такое могло случиться?». Fu-Tzu ответил: «Нет необходимости забирать водяной шланг, когда дом не горит». Это не следует рассматривать как поощрение неаккуратного программирования, а скорее как предупреждение против невротического соблюдения эмпирических правил.

Дополнительную информацию о функциональном реактивном программировании можно найти на следующих ресурсах:

Заключение

Мы закончим ещё одной отличной цитатой из первого издания Eloquent JavaScript: «Студент долгое время сидел за своим компьютером, мрачно хмурился и пытался написать красивое решение сложной проблемы, но не мог найти правильный подход. Fu-Tzu ударил его по затылку и крикнул: 'Введите что-нибудь!'. Студент начал писать уродливое решение, и после того, как он закончил, он внезапно понял прекрасное решение».

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

Используя этот Справочник в качестве отправной точки, вы можете начать использовать представленные концепции и парадигмы программирования для повышения своего уровня владения JavaScript. Если по описанным темам что-либо ещё неясно, пожалуйста, обратитесь к ссылкам в каждом разделе за дополнительными ресурсами. Позже мы рассмотрим новые концепции в следующей статье Справочника современных концепций JavaScript!


Читайте нас на Медиуме, контрибьютьте на Гитхабе, общайтесь в группе Телеграма, следите в Твиттере и канале Телеграма, скоро подъедет подкаст. Не теряйтесь.