Underscore: Ещё больше функциональщины в JavaScript!
kayo — Ср, 22/02/2012 - 22:23
Сегодня нас с вами ждёт увлекательный рассказ о том, как не только преодолеть трудности переносимости JavaScript кода между различными средами исполнения, но и писать код в истинно функциональном стиле. Разговор пойдёт о библиотеке Underscore.
Нет, это не ещё один браузерный фреймворк для работы с DOM, вроде JQuery и ему подобных, библиотека также не добавляет отсутствующую функциональность в стандартные объекты как, например, Prototype. Всё, что делает Underscore — предоставляет набор полезных функций для написания эффективных программ и удобный способ работы с ними. Библиотека использует нативные реализации вызовов, если таковые имеются. Прежде всего нас будут интересовать возможности, связанные с функциональным программированием, поэтому менее существенные вещи будут опущены.
Работа с данными
Библиотека работает с тремя основными типами данных: с массивами, объектами и функциями. Первые два в терминах библиотеки представляют собой тип коллекция (collection) или список (list). Многие общие функции работают в целом с коллекциями, а другие только с массивами или только с объектами, это нужно учитывать. Объекты, как правило, разбираются в виде пар значение-ключ, а массивы аналогичным образом в виде пар значение-индекс.
Способы вызова, цепочки
Всё, что у нас есть, это объект _. Все функции доступны как свойства этого объекта. Для любителей ООП имеется возможность обернуть данные и вызывать функции непосредственно как методы полученного объекта.
Итак, смотрим пример:
/* непосредственный вызов */ _.функция(коллекция, параметры…) /* вызов в объектном стиле */ _.chain(коллекция).функция(параметры…) /* или более кратко */ _(коллекция).функция(параметры…)
Полезной особенностью объектного стиля является то, что функции также возвращают обёрнутые данные, что позволяет создавать цепочки вызовов с целью последовательной обработки, например:
/* такую программу */ _.функцияN(_.функция2(_.функция1(коллекция, параметры…), параметры…), параметры…) /* можно представить как */ _(коллекция).функция1(параметры…).функция2(параметры…).функцияN(параметры…).value() /* последний вызов value() «разворачивает» данные */
Принципы работы с данными
Теперь посмотрим, как организованы вызовы функций. Как правило, если функция библиотеки обрабатывает данные путём применения переданной ей функции, то имеется возможность передать также и так называемый контекст, то есть фактически значение this, которое будет доступно в ней.
Все функции работы с данными поделим условно на следующие классы:
- Отображение (map, pluck, invoke) — отображает одну коллекцию в другую с теми же элементами, но преобразованными правилом.
- Свёртывание (reduce, max, min, find, first) — сужает коллекцию по некоторому правилу до единственного значения, не обязательно атомарного.
- Фильтрация (filter, reject, uniq, without, difference, intersection) — фильтрует элементы коллекции по определённому правилу.
- Расширение (extend, default, flatten, union) — расширяет коллекцию новыми элементами, вычисленными по правилу над имеющимися.
- Изменение порядка (sort, shuffle) — меняет порядок следования элементов.
- Анализ (any, all, include, size) — получает некоторую общую информацию о коллекции.
Вот пара традиционных для ФП вещей:
/* возвращает список, каждый элемент которого увеличен на четыре */ _.map([1, 2, 3], function(item){ return this + item; }, 4); /* возвращает сумму элементов списка, первый элемент складывается с нулём */ _.reduce([1, 2, 3], 0, function(memo, item){ return memo + item; });
Функция map «прогоняет» каждый элемент списка через указанную функцию, возвращая список с тем же числом элементов, но уже с новыми значениями.
Функция reduce «схлопывает» список до единственного значения, которое, впрочем, не обязано быть атомарным. Каждый вызов переданной функции получает результат предыдущего вызова и очередной следующий элемент списка, при первом вызове в качестве результата вычисления берётся начальное значение (второй аргумент).
Давайте теперь сконструируем последовательность вычислений:
var dec = function(a){ return a - this; }, gt = function(){ return a > this; }; _([6, 1, 2, 0, 3, 4, 5, 6]) .chain() /* делаем полноценную цепочку */ /* без этого у меня были недоступны функции массивов */ .map(dec, 2) /* уменьшаем все значения на 2 */ .filter(gt, 0) /* оставляем только положительные */ .uniq() /* убираем дубли */ .without(3) /* убираем 3 */ .value();
Вызов filter оставляет только те элементы, для которых переданная функция вернёт true. Функция uniq оставляет только единственные экземпляры значений, а without убирает переданные ей.
Функций работы с коллекциями, с массивами и объектами достаточно много, нет смысла описывать их здесь все, поэтому остановимся на самых полезных.
Операции над коллекциями
Остановимся подробнее на некоторых полезных операциях с коллекциями:
- _.map(коллекция, функция(значение, ключ, коллекция)[, контекст]) — функция применяется к каждому элементу, в результирующую коллекцию попадают возвращаемые значения.
- _.reduce(коллекция, функция(предыдущее_значение, текущее_значение, ключ, коллекция), начальное_значение[, контекст]) — функция применяется к каждому элементу коллекции по очереди, возвращаемое значение передаётся в каждый следующий вызов в качестве предыдущего. Вариант: _.reduceRight — разбирает элементы коллекции в обратном порядке.
- _.filter(коллекция, функция(значение, ключ, коллекция)[, контекст]) — функция применяется к каждому элементу и если возвращает истину, элемент останется в результирующей коллекции. Вариант: _.reject — убирает элементы для которых функция вернула истину.
- _.pluck(коллекция, имя_свойства_элементов) — вариация на тему _.map, возвращает коллекцию, состоящую из свойств элементов с указанным именем.
- _.find(коллекция, функция(значение, ключ, коллекция)[, контекст]) — вариация на тему _.filter, возвращает первый элемент, для которого функция вернула истину.
- _.sortBy(коллекция, функция(значение, ключ, коллекция)[, контекст]) — сортирует элементы коллекции по возвращаемым функцией значениям.
- _.min, _.max(коллекция, функция(значение, ключ, коллекция)[, контекст]) — возвращает элемент коллекции, для которого функция вернула экстремальное значение.
- _.all(коллекция, функция(значение, ключ, коллекция)[, контекст]) — возвращает истину если функция вернула истину для всех элементов.
- _.any(коллекция, функция(значение, ключ, коллекция)[, контекст]) — возвращает истину если функция вернула истину хотя бы для одного элемента.
Операции над объектами
Вот некоторые полезные вещи, которые мы можем делать с объектами:
- _.keys(объект) — извлекает ключи в виде массива.
- _.values(объект) — извлекает значения в виде массива.
- _.functions(объект) — извлекает массив функций.
- _.clone(объект) — создаёт одноуровневую копию объекта.
- _.extend(объект, исходные объекты) — копирует все свойства исходных объектов в первый.
- _.defaults(объект, свойства по умолчанию) — заполняет несуществующие в объекте свойства значениями по-умолчанию.
Операции над массивами
Осталось рассмотреть полезные функции работы с массивами:
- _.first(массив) — возвращает первый элемент массива.
- _.rest(массив) — возвращает хвост массива (то есть весь массив кроме первого элемента).
- _.compact(массив) — возвращает массив очищенный от пустых элементов (то есть без 0, null, false, "", undefined, NaN).
- _.flatten(массив, на один уровень) — раскрывает вложенность элементов массива полностью или на один уровень.
- _.without(массив, элементы) — убирает элементы из массива.
- _.uniq(массив) — убирает дублирующиеся элементы из массива.
- _.union(массивы) — объединение массивов (остаются элементы, присутствующие хотя бы в одном).
- _.intersection(массивы) — пересечение массивов (остаются только общие для всех элементы).
- _.difference(массив, массивы) — разница (остаются элементы, присутствующие в первом массиве, но отсутствующие в последующих)
- _.range([начало, ]конец[, шаг]) — генерирует массив по правилу: for(элемент = начало; элемент < конец; элемент+=шаг).
- _.zip(массив, массив, массив) — выполняет операцию, похожую на транспонирование матриц, если рассматривать переданные массивы как строки, то они станут столбцами.
Работа с функциями
Полезной особенностью функциональной парадигмы является тот факт, что функции это обычные значения, с которыми тоже можно производить различные операции, естественно имеющие практический смысл. Посмотрим, что нам может предложить в этом плане библиотека Underscore.
Действия над функциями
Мы можем сделать композицию из нескольких функций, то есть получить такую функцию, которая эквивалентна последовательному вызову каждой из них, так что следующая получит результат вычисления предыдущей:
var ge_0 = function(v){ return Math.min(v, 0.0); }, le_1 = function(v){ return Math.max(v, 1.0); }, bw_01 = function(){ return Math.max(Math.min(v, 0.0), 1.0); }, bw_01_c = _.compose(ge_0, le_1); /* функции bw_01 и bw_01_c эквивалентны */
Естественно, в композиции могут участвовать сколько угодно функций.
Можно оборачивать функцию другой, которой в качестве первого аргумента будет доступна первая функция, например:
var format_integer = function(v){ return ''+Math.round(v); }, format_natural = _.wrap(format_integer, function(format, v){ return v > 0 ? format(v) : '<0'; }); console.log(format_integer(1.2), format_natural(1.2)); /* увидим 1, 1 */ console.log(format_integer(-2), format_natural(-2)); /* увидим -2, <0 */
Оптимизация вычислений
Однако, помним, что операции над функциями служат не только и не столько нашему удобству, сколько оптимизации вычислений. Весьма полезная особенность — иметь функцию, выполняемую только один раз, первый (_.once) или последний (_.after).
var init = _.once(function(){ return { width: scale*w, height: scale*h }; }); var size = init(); /* все дальнейшие вызовы init будут возвращать */ /* единожды возвращённое значение, */ /* код выполняться не будет */ /* пример из документации по библиотеке */ var renderNotes = _.after(notes.length, render); _.each(notes, function(note) { note.asyncSave({success: renderNotes}); }); /* выполняем рендеринг только один раз после того, */ /* как все заметки сохранены */
Более общая и более полезная вещь memoize, которая позволяет запоминать результаты предыдущих вычислений функции, для ускорения последующих вызовов её с ранее имевшими место быть аргументами:
/* хороший пример из документации */ var fibonacci = _.memoize(function(n) { return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); });
Следует учесть, что в качестве ключа для кеширования вычисленных значений используется первый аргумент, поэтому, если в нашу функцию передаётся несколько аргументов, то нужно в качестве второго параметра передать так называемую хеш-функцию, которая должна вычислять ключ:
var awesome_computation = _.memoize(function(a, b, c){ /* что-то очень тяжелое */ }, function(a, b, c){ return ''+a+'/'+b+'/'+c; });
Связывание функций
Под связыванием здесь подразумевается связывание функции с контекстом. Это ещё одна простая, удобная и полезная вещь, которую делает Underscore. Если функцию связать с некоторым объектом, то независимо от того, в каком контексте она будет выполнена, значением this внутри этой функции будет связанный объект. Мы можем связывать отдельную функцию с отдельным объектом или поля объекта с ним самим. В первом случае также можно выполнить частичное применение аргументов, например:
var pointer = { x: 120, y: 205 }; var cursor = _.bind(function(offset, origin){ console.log(this); return { x: this.x - offset.x + origin.x, y: this.y - offset.y + origin.y }; }, pointer, { x: -3, y: -5 }); console.log(cursor.call(window, {x:10, y:12}));
Второй способ связывания работает на объекте, и, если не передать в качестве аргументов, какие функции объекта требуется связать с ним, будут связаны все:
var obj = { display_data: function(data){ /* что-то делаем */ } }; _.bindAll(obj); $.get('/path/to/data.json', obj.display_data);
Также в библиотеке есть несколько функций, которые реализуют отложено выполняемые функции, работают они в основном посредством таймеров:
- _.delay(функция, задержка, аргументы) — создаёт функцию, выполняемую с указанной задержкой. Как бонус можно частично «применить», передав аргументы.
- _.defer(функция, аргументы) — выполняет функцию в тот момент, когда стек вычислений пуст. Полезно для «разгрузки» среды исполнения и предотвращения блокировок.
- _.throttle(функция, задержка) — создаёт функцию, которая выполняется не чаще, чем раз в указанное время. Весьма полезно использование в качестве всяческих функций обновления, вызываемых в качестве обработчиков событий и подразумевающих сложные вычисления. Функция не будет вызываться непосредственно, тем самым мы значительно снизим нагрузку на среду исполнения при обработке часто возникающих событий.
- _.debounce(функция, задержка) — реализуют функцию, выполняемую с задержкой на указанное время с момента последнего вызова. Столь же полезная вещь, как предыдущая, но с несколько иной логикой работы. Покуда вызовы будут происходит быстрее указанной задержки, выполнение будет всякий раз откладываться.
Пожалуй, мы рассмотрели все функции работы с функциями, не хватает разве что только карринга, но он не так часто нужен. Мы видим, что функции работы с функциями генерируют функции в своей узкой области видимости, что полезно, поскольку предотвращает создание замыканий в нашем коде, где мы их вызываем.
Вместо заключения
Функциональная парадигма позволяет значительно сократить объём кода, сделать его проще и понятнее. Думать о преобразовании данных как о цепочке применения функций просто и естественно для человеческого мозга, поскольку позволяет быстро переключаться от общего к частному и наоборот.
Разумно сочетая императивные последовательности операций с функциональным вычислением, можно писать более простые и эффективные программы на JS, свободные от нежелательных побочных эффектов. Не говоря уже о том, что функциональное программирование само по себе весьма занятно и увлекательно.
Underscore работает не только в браузерах, но и в серверных средах типа Node.JS, подключается как в виде скрипта, так и посредством AMD.
Стоит отметить, что библиотека Underscore реализована также и для других языков помимо JavaScript, например, для Lua, Perl, PHP.

Я делал немного проще
Са П. (не проверено) — Ср, 13/08/2014 - 18:41Отправить комментарий