Ленивый, компонуемый и модульный JavaScript
В этой статье остановимся на использовании четырех возможностей ECMAScript: итераторах, генераторах, «жирных» стрелочных функциях и операторе for-of в сочетании с функциями высшего порядка, композициями функций, отложенными вычислениями.
11К открытий11К показов
Хорошей практикой считается делать код компонуемым и модульным. Он упрощается и к его частям становится проще обращаться несколько раз. Для JavaScript-разработчиков тоже появились инструменты для использования этого подхода.
Остановимся на использовании четырех возможностей ECMAScript: итераторах, генераторах, «жирных» стрелочных функциях и операторе for-of
в сочетании с функциями высшего порядка, композициями функций, отложенными вычислениями.
Прежде чем погрузиться с головой в пример, рассмотрим некоторые общие понятия.
Функции высшего порядка
Функция высшего порядка — это функция, которая удовлетворяет хотя бы одному из условий:
- принимает в качестве аргументов одну или более функций;
- возвращает функцию как результат.
Вы наверняка сталкивались с функциями высшего порядка, если писали обработчик событий или применяли Array.prototype.map()
.
Например, функция, попадая в Array.prototype.map
, ничего не знает о её структуре и методах. Единственное знание — механизм обработки своих входящих данных, поэтому может быть неоднократно применима как для отдельных значений, так и коллекций.
Композиции функций
Композиция функций — применение одной функции к результату другой. В результате чего из простых функций составляются сложные.
Например, есть две функции: f
и g
. Результат композиции (f, g)
— функция f(g(x))
, которую так же можно использовать в композиции или передать как параметр функции высшего порядка.
Допустим, поставили задачу написать программу, которая на входе принимает файл и возвращает массив, содержащий количество гласных в каждом слове на каждой строке. Одно из решений — написать одну большую функцию, которая делает это.
В монолитном решении, представленном ниже, используется новый оператор for-of
вместо привычного for-loop
для перебора значений массива. Итератор — контейнер, который реализует протокол последовательного перебора и возвращает с помощью оператора yield
значения по одному (массивы, строки, генераторы и т. д.).
Это решение не расширяемое, не масштабируемое и не содержит повторно используемых компонентов. Альтернативный подход состоит в использовании функций высшего порядка и композиции.
listOfWordsByLine
возвращает массив массивов, где каждый элемент соответствует массиву слов, составляющих строку. Например:
В примере vowelCount
подсчитывает количество гласных в слове, vowelOccurrences
использует vowelCount
на выходе listOfWordsByLine
для расчета гласных в каждой строке.
Профит второго способа — универсальные функции, которые будут полезны в дальнейшей работе и будут комбинироваться вместе для решения больших задач.
Таким образом, они приводят к подходу «снизу вверх», в результате чего код становится компонуемым и модульным.
Отложенные вычисления
Отложенные («ленивые») вычисления — операции, выполнение которых откладывается до тех пор, пока не нужен результат.
Рассмотрим на примере «ленивую» обработку данных и построение «ленивых» цепочек вычислений (pipelines).
Дан список целых чисел. Необходимо возвести в квадрат элементы списка и вывести сумму первых четырех полученных значений.
Для написания «ленивой» реализации выясним, когда понадобится произвести вычисления. При суммировании первых четырёх квадратов нужно возвести в квадрат эти элементы, поэтому операцию возведения в квадрат отложим до начала суммирования.
Возводим в степень элементы только тогда, когда начинается суммирование. За счёт контроля итерации и yield
обрабатываются только те элементы, которые будут участвовать в итоге. Итерация реализуется таким образом, что элементы возвращаются с помощью оператора yield
по одному вплоть до получения сигнала об отсутствии элементов для вывода. Протокол инкапсулируется в объект итератора, который содержит одну функцию — next
, принимающую нулевые значения. Следующий элемент возвращается, только при наличии элементов.
Функция squareAndSum
принимает в качестве входных данных итератор и n
(число элементов в сумме). С помощью вызова метода .next()
n
раз получает из итератора n
значений, возводит каждый из элементов в квадрат и суммирует их.
GetIterator
возвращает итератор, сформированный из нашего списка.
squareAndSumFirst4
использует getIterator
и squareAndSum
, чтобы вернуть сумму первых четырех чисел из входного списка, возведённых в квадрат «ленивым» способом. Использование итераторов позволяет внедрять структуры данных, которые могут вернуть с помощью оператора yield
бесконечные значения.
Необходимость выполнения описанных выше действий каждый раз, когда нам нужен итератор, усложняет написание кода. К счастью, ES, начиная с версии 6, предлагает простой способ описания итераторов — генераторы.
Генератор — функция, работу которой можно приостановить, а потом возобновить. Причём генератор может выдать значения несколько раз в ходе исполнения с помощью ключевого слова yield
. При вызове возвращает объект-генератор. С помощью метода .next
получается следующее значение. В JavaScript генераторы создаются путём определения функции с *
.
Генераторы поддерживают оба протокола, поэтому получить значения можно с помощью оператора for-of
.
Теперь реализуем задачу, которая покажет, как эти три подхода помогают очистить код приложения.
Пример: задача и решение
Исходные данные:
- файл, который на каждой строке содержит имя пользователя и по размеру превышает объём оперативной памяти используемого устройства;
- функция, которая считывает блок данных с диска и возвращает его же, дополнив символом новой строки.
Задача:
- Получить имена пользователей, которые начинаются с «A», «E» или «M».
- Выполнить запрос с использованием полученных данных к странице
http://jsonplaceholder.typicode.com/users?username=<username>
. - Применить к первым четырём ответам сервера заданный набор из четырёх функций.
Содержимое файла:
Разбиваем задачу на блоки поменьше, чтобы для каждого написать отдельную функцию. В результате получаем следующие функции:
- Первая возвращает каждое имя (
getNextUsername
). - Вторая отбирает имена, которые начинаются с «A», «E»или «M» (
filterIfStartsWithAEOrM
). - Третья делает сетевой запрос и возвращает
Promise
, объект-заглушку для вывода результата вычисления (makeRequest
).
Эти функции оперируют значениями. Для того, чтобы применить их к списку, введём три функции высшего порядка:
- Первая выбирает элементы списка на основе заданных параметров (
filter
). - Вторая применяет функцию к каждому элементу списка (
map
). - Третья применяет функции из одного итератора к данным другого итератора (zipWith c функцией упаковки).
«Ленивость» этого подхода может принести пользу, так как сетевые запросы делаются не для всех подходящих под критерии фильтрации имен.
Итак, у нас есть массив функций, которые должны выполняться для обработки окончательных ответов, и функция, которая возвращает «ленивым» способом блоки данных. Напишем функцию с применением генераторов для получения имен пользователей, используя «ленивый» подход.
Теперь для работы со значениями необходимы следующие функции:
- Первая возвращает
True
, если значение удовлетворяет критериям фильтра,False
— в противном случае. - Вторя возвращает URL-адрес при получении имени пользователя.
- Третья при получении URL-адреса делает запрос и возвращает
Promise
для этого запроса.
Promise
— «контейнер» для хранения значения выполняемой операции, которое появится в будущем. Интерфейс Promise
позволяет определить, какие действия выполнять после успешного завершения операции или сбоя. Если операция пройдёт успешно, вызывается обработчик удачного результата со значением операции. В противном случае вызывается обработчик ошибки.
Теперь напишем функции высшего порядка, которые обеспечат работу с «ленивыми» списками. Их задача — отложить выполнение до тех пор, пока не появится запрос. В этом случае нужны значения по требованию, поэтому на помощь придут генераторы.
Необходимые функции написаны. Используем композицию, чтобы решить поставленную задачу.
lazyComposedResult
— «ленивая» цепочка вычислений (pipeline), составленная из композиций функций. Ни одно звено не выполнится, пока не запущен верхний блок композиции, то есть lazyComposedResult
. Мы сделали только четыре вызова, хотя результат фильтрации может содержать более четырех значений.
В итоге получили лаконичное решение задачи, в котором появились функции высшего уровня, композиции и повторно используемые функции.
11К открытий11К показов