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

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

Эта статья о функциональных концепциях JavaScript. Некоторые из них встроенны в языки программирования, другие дополнительно установляются, но все они хорошо распространены в чисто функциональных языках, как Haskell. Первое, на что я хотел бы обратить внимание, это на то, что я подразумеваю под термином чисто функциональный язык. Такие языки «безопасны», они не вызывают побочных эффектов, то есть определение выражения ничего не изменит во внутреннем состоянии и не приведет к другому результату одного и того же выражения после повторного вызова. Это кажется очень странным и бесполезным таким серьёзным парням, как я, но на самом деле в этом есть достаточно много преимуществ:

  1. Согласованность. Здесь не будет дедлоков или race conditions оттого, что нам не нужна блокировка – данные неизменные. Это уже многообещающе. 
  2. Модульное тестирование. Мы можем писать юнит-тесты и не волноваться о состоянии просто потому, что здесь его нет. Нам нужно беспокоиться только об аргументе функций, которые мы тестируем.
  3. Устранение дефектов. Простой трассировки стека полностью достаточно.
  4. Прочная теоретическая база. Функциональные языки базируются на лямбда-исчислении – формальной системе. Этот теоретический фундамент очень просто доказывает правильность написания программ (например, с использованием индукции).

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

Анонимные функции

Кажется, функциональное программирование становится все более популярным. Дуже в Java 8 скоро появятся анонимные функции, в C# они есть уже очень давно. Анонимная функция – это функция, которая определяется без идентификатора. В JavaScript эта концепция уже настроена. Если вы использовали JavaScript не только для наипростейших задач, я уверен, что вы тоже знаете о нем. Когда вы используете jQuery, вот что вы точно печатаете сначала:

  1. $(document).ready(function () {
  2.     //do some stuff
  3. });

Функция, переданная в $(document).ready и есть анонимной функцией. Этот концепт очень выгодный в некоторых случаях, когда мы хотим действовать за принципом DRY (Don't repeat yourself; if you're repeating yourself, you're doing it wrong).
Больше о прохождении функций в следующем разделе.

 

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

Функции высшего порядка – это функции, которые принимают функции в качестве аргументов или возвращают функции. Мы можем возвратить и провести функции, как аргументы в C#, Java 8, Python, Perl, Ruby… Самый известный язык программирования – JavaScript имеет эти встроенные функции уже очень давно. Вот стандартный пример:

  1. function animate(property, duration, endCallback) {
  2.     //Animation here...
  3.     if (typeof endCallback === 'function') {
  4.         endCallback.apply(null);    }
  5. }
  6. animate('background-color', 5000, function () {
  7.     console.log('Animation finished');
  8. });

В коде выше есть функция animate. Она принимает в качестве свойств аргументов, которые должны быть анимированными, продолжительность и колбек, к которым мы должны ссылаться, когда анимация будет завершена. Мы также имеем этот пример в jQuery. Есть множество методов jQuery, которые принимают функции как аргументы, например $.get:

  1. $.get('http://example.com/test.json', function (data) {
  2.     //processing of the data
  3. });

Колбеки вездесущи в JavaScript, они идеально подходят к асинхронному программированию, например, управлению событиями, ajax запросам, управлениями клиентами (Node.js) и т.д. Как я уже упоминал, можно избежать повторения, используя колбеки. Они особенно полезны, когда вы хотите, чтобы кусок вашего кода вел себя иначе в зависимости от условий. 

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

  1. /* From Asen Bozhilov's lz library */
  2. lz.memo = function (fn) {
  3.     var cache = {};
  4.     return function () {
  5.         var key = [].join.call(arguments, '§') + '§';
  6.         if (key in cache) {
  7.             return cache[key];
  8.         }
  9.         return cache[key] = fn.apply(this, arguments);
  10.     };
  11. };

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

  1. var foo = 1;
  2. function bar(baz) {
  3.     return baz + foo;
  4. }
  5. var cached = lz.memo(bar);
  6. cached(1); //2
  7. foo += 1;
  8. cached(1); //2

У нас есть функция bar, которая принимает единый аргумент – baz и возвращает сумму baz и global foo. Когда мы используем memo, мы готовим bar к кэшированию и сохраняем ссылку на кэшированную копию в переменной cashed. Когда мы вызываем переменную cashed впервые, она исчисляется аргументом 1, ее тело вызывается и поэтому результатом будет 2. После этого мы повышаем foo и вызываем cashed снова. Теперь у нас одинаковый результат (как и должно быть в чисто функциональных языках), но это неправильный результат. 

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

Замыкания

Давайте снова посмотрим на memo. У нас есть переменная cashed, она определена в лексической области функции, которая возвращает кэшированную функцию. Эта переменная также доступна с возвращенной функции, потому что создано замыкание. Это еще один элемент из функционального программирования. И он весьма распространен. Еще один способ для реализации приватности в JavaScript – это использование замыканий:

  1. var timeline = (function () {
  2.     var articles = [];
  3.     function sortArticles() {
  4.         articles.sort(function (a, b) {
  5.             return a.name - b.name;
  6.         });
  7.     }
  8.     return {
  9.         getArticles: function () {
  10.             return articles;
  11.         },
  12.         setArticles: function (articleList) {
  13.            articles = articleList;
  14.            sortArticles();
  15.         }
  16.     };
  17. }());

В примере выше был объект, который называется таймлайн. Это результат немедленно вызываемых функций (IIFE) которые возвращают объект со свойствами getArticles и setArticles, которые представляют текущий публичный интерфейс таймлайна. Внутри лексической области IIFE есть определение массива articles и функция сортировки, которую нельзя вызвать прямо используя объект таймлайна. 

Я завершу тему о функциях высшего порядка, рассказав о ECMAScript 5. Спустя годы у JavaScript появляются еще функциональные элементы. Наверное, первое, что приходит в голову, когда слышишь о функциональном программировании, это функция map. Она принимает за аргументы анонимные функции и список. Она использует функцию ко всем элементам из списка. map – официально часть ECMAScript 5.

Вот ее стандартное использование:

  1. [1,2,3,4].map(function (a) {
  2.     return a * 2;
  3. });
  4. //[2,4,6,8]

В коде выше map дублирует массив. map – не единственная функция, которая так типична для языков функционального программирования и дополнена к ECMAScript 5, также есть функции фильтра и сокращения.

Рекурсия

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

  1. function factorial(n) {
  2.     if (n <= 1) return 1;
  3.     return n * factorial(n - 1);
  4. }

Выше – базовый пример, который показывает имплементацию факториала с использованием рекурсии. Этот концепт весьма популярен, потому я пропущу детальное объяснение. 

 

Управление состояниями (Монады)

Конечно, как мы видим на примере с мемоизацией, JavaScript не чисто функциональный язык (наверное, поэтому он такой популярный), потому что он имеет изменчивые данные и состояния. Обычно, чисто функциональные языки программирования как Haskell управляют состояниями с помощью монад. Вот реализации монад в JavaScript. Возьмем пример Douglas Crockford:

  1. /* Code by Douglas Crockford */
  2. function MONAD(modifier) {
  3.     'use strict';
  4.     var prototype = Object.create(null);
  5.     prototype.is_monad = true;
  6.     function unit(value) {
  7.         var monad = Object.create(prototype);
  8.         monad.bind = function (func, args) {
  9.             return func.apply(
  10.                 undefined,
  11.                 [value].concat(Array.prototype.slice.apply(args || []))
  12.             );
  13.         };
  14.         if (typeof modifier === 'function') {
  15.             modifier(monad, value);
  16.         }
  17.         return monad;
  18.     }
  19.     unit.method = function (name, func) {
  20.         prototype[name] = func;
  21.         return unit;
  22.     };
  23.     unit.lift_value = function (name, func) {
  24.         prototype[name] = function () {
  25.             return this.bind(func, arguments);
  26.         };
  27.         return unit;
  28.     };
  29.     unit.lift = function (name, func) {
  30.         prototype[name] = function () {
  31.             var result = this.bind(func, arguments);
  32.             return result && result.is_monad === true ? result : unit(result);
  33.         };
  34.         return unit;
  35.     };
  36.     return unit;
  37. }

Вот маленькая демка, которая показывает, как мы можем создать I/O монаду:

  1. var monad = MONAD();
  2. monad(prompt("Enter your name:")).bind(function (name) {
  3.     alert('Hello' + name + '!');
  4. });

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

Schönfinkelization 
(или просто каррирование)

Каррирование (Schönfinkelization или Currying) это функциональное преобразование, которое позволяет заполнять аргументы функции шаг за шагом. Когда функция принимает последний аргумент, она возвращает результат. Эта функция была введена Moses Schönfinkel и позднее еще раз открыта Haskell Curry (потому и каррирование). Вот пример Stoyan Stefanov реализации ее в JavaScript:

  1. /* By Stoyan Stafanov */
  2. function schonfinkelize(fn) {
  3.     var slice = Array.prototype.slice,
  4.         stored_args = slice.call(arguments, 1);
  5.     return function () {
  6.         var new_args = slice.call(arguments),
  7.             args = stored_args.concat(new_args);
  8.         return fn.apply(null, args);
  9.     };
  10. }

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

  1. function quadraticEquation(a, b, c) {
  2.     var d = b * b - 4 * a * c,
  3.         x1, x2;
  4.     if (d < 0) throw "No roots in R";
  5.     x1 = (-b - Math.sqrt(d)) / (2 * a);
  6.     x2 = (-b + Math.sqrt(d)) / (2 * a);
  7.     return {
  8.         x1: x1,
  9.         x2: x2
  10.     }
  11. }

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

  1. var temp = schonfinkelize(quadraticEquation, 1);
  2. temp = schonfinkelize(temp, -2);
  3. temp(1); // { x1: 1, x2: 1 }

Если вы хотите использовать встроенные в язык свойства вместо функции каррирования, вы можете также использовать Function.prototype.bind. С тех пор как он установлен в прототип функции конструктора всех функций, вы можете использовать его, как метод во всех функциях. Метод bind создает новую функцию со специфическими контекстом и параметрами. Например, у нас может быть такая функция:

  1. var f = function (a, b, c) {
  2.   console.log(this, arguments);
  3. };    

Теперь мы применяем метод bind:

  1. var newF = f.bind(this, 1, 2);
  2. newF(); //window, [1, 2]
  3. newF = newF.bind(this, 3)
  4. newF(); //window, [1,2,3]
  5. newF(4); //window, [1,2,3,4]

Нужно быть осторожным с методом bind, потому что он не везде поддерживается. Созданная на kangax ES5 таблица совместимости IE9+ показывает, где поддерживается метод bind.

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

Сопоставление с образцом

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

  1. factorial 0 = 1
  2. factorial n = n * factorial (n - 1)

Выглядит довольно круто. Вы делите одно крупное задание на кучу маленьких и упрощаете себе решение. Когда я начал писать эту статью, у меня была идея реализовать что-то подобное в JavaScript, но я узнал, что кое-кто уже сделал это до меня. Вот пример от Bram Stein’s funcy:

  1. var $ = fun.parameter,
  2.     fact = fun(
  3.         [0, function ()  { return 1; }],
  4.         [$, function (n) { return n * fact(n - 1); }]
  5.     );

У нас есть специальная переменная для параметров, которые находятся внутри функции fun.parameter. Когда мы вызываем факт с “ – function () { return 1; } будет вызвана, с другой стороны, функция (n) { return n * fact(n – 1); }. Круто, не так ли?
Надеюсь, тут достаточно ясно показано все функциональные аспекты JavaScript и то, насколько красивыми и простыми они могут быть.

Автор - Minko Gechev
Оригинал статьи