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

В данном посте я решил собрать как можно больше теории о ФП, особенно применение его в JavaScript, и понять почему эта парадигма становится все более популярной. Рост популярности хорошо видно на следующем графике с Google Trends:

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

Это определение звучит как-то заумно, поэтому есть другое – Функциональное программирование (ФП) – это процесс создания программного обеспечения используя чистые функции (pure functions), избегая общего состояния (shared state), изменяемых данных (mutable data), а также побочных эффектов ( side-effects).

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

Основные понятия:

  • Функции высших порядков (Higher-order Functions)
  • Функции первого класса (First-Class Functions)
  • Чистые функции (pure functions)
  • Побочный эффект функции (side-effects)
  • Неизменное состояние (immutable state)
  • Shared State
  • Замыкания (closure)
  • Рекурсия
  • Частичное применение функции (Partial function)
  • Функтор
  • Монада

Функции высших порядков (Higher-order Functions)

Функции высших порядков (Higher-order Functions) – это такие функции, которые могут принимать в качестве аргументов и возвращать другие функции. Функции высших порядков позволяют использовать карринг (об этом чуть позже) – преобразование функции от пары аргументов в функцию, которая берет свои аргументы по порядку. Поскольку в JavaScript функции – это объекты, то мы можем их легко передавать в качестве аргументов другим функциям и возвращать как результат.

Вот простой пример функции, которая возвращает функцию:


function makeAdder(base) {
 return function(num) {
 return base + num;
 }
}

И пример ее использования:

var add2 = makeAdder(2);
add2(3); //5
add2(7); //9

А вот достаточно известный пример функции высшего порядка:

var el = document.getElementById("btn");

el.addEventListener("click", function (event){

});

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

Функции первого класса (First-Class Functions)

Функции первого класса (First-Class Functions) – значит, что вы сможете сохранять функции в переменные. В JavaScript это используется практически везде:


var add = function (a, b) {
    return a + b
}

Чистые функции (pure functions)

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

  • Если результат чистой функции не используется, ее вызов может быть удален без ущерба для других частей скрипта.
  • Результат вызова чистой функции может быть сохранен в таблице значений вместе с аргументами вызова. Если в дальнейшем функция вызывается с этими же аргументами, ее результат может быть взят прямо из таблицы, а не вычисляя значение заново (иногда это называется принципом прозрачности ссылок). Ценой небольшого расхода памяти можно существенно увеличить производительность и уменьшить порядок роста некоторых рекурсивных алгоритмов.
  • Если нет никаких shared данных между двумя чистыми функциями, то порядок их исчисления можно поменять или распараллелить (иначе говоря вычисления чистых функций удовлетворяет принципам thread-safe).

//pure function

var add = function (a, b) {

    return a + b;

};

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

Побочный эффект функции (side-effect)

Побочный эффект функции (side-effect) – возможность в процессе выполнения своих вычислений: читать и модифицировать значения глобальных переменных, осуществлять операции ввода-вывода, реагировать на исключительные ситуации, вызвать их обработчиков. Если вызвать функцию с побочным эффектом дважды с одним и тем же набором значений входных аргументов, может случиться так, что в качестве результата будут возвращены разные значения. Такие функции называются недетерминированными функциями с побочными эффектами.

Неизменяемое состояние (immutable state)

Неизменяемое состояние (immutable state) означает, что вы вообще не можете менять любые состояния (хотя можете создавать новые).

var statement = "I am an immutable value";
var otherStr = statement.slice(8, 17);

Shared State

Shared State – это переменная, объект или область памяти существующая в общем скоупе (scope или области видимости) или свойство объекта которое может передаваться в другие области видимости. Shared scope может включать глобальный скоуп и closure скоуп.


// With shared state, the order in which function calls are made
// changes the result of the function calls.
const x = {
val: 2
};

const x1 = () => x.val += 1;

const x2 = () => x.val *= 2;

x1();
x2();

console.log(x.val); // 6

// This example is exactly equivalent to the above, except...
const y = {
val: 2
};

const y1 = () => y.val += 1;

const y2 = () => y.val *= 2;

// ...the order of the function calls is reversed...
y2();
y1();

// ... which changes the resulting value:
console.log(y.val); // 5

Замыкания (closure)

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


var add = function(a) {
    return function(b) {
        return a + b
    }
}

var add2 = add(2)
add2(3) // => 5

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

Рекурсия

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


function factorial(num)
{
 // If the number is less than 0, reject it.
 if (num < 0) {
 return -1;
 }
 // If the number is 0, its factorial is 1.
 else if (num == 0) {
 return 1;
 }
 // Otherwise, call this recursive procedure again.
 else {
 return (num * factorial(num - 1));
 }
}

var result = factorial(8);

Частичное применение функции (Partial function)

Частичное применение функции (Partial function) – создание какой-то функции на базе указанной, где некоторые аргумент заменены конкретными значениями, к примеру:

function partial_one_arg(f, a) {
   return function(b) {
      return f(a, b);
   }
}

min0 = partial_one_arg(Math.min, 0);
min1 = partial_one_arg(Math.min, 1);

alert([min0(-5), min0(5)]); // вернёт [-5, 0]
alert([min1(-5), min1(5)]); // вернёт [-5, 1]

В JavaScript для реализации частичного применения можно использовать Function.prototype.bind. Этот метод используется, в большинстве случаев, когда необходимо избавиться от лишних присваиваний var that = this или var self = this, встречающихся в коде на каждом шагу. Вот простой пример:

this.setup = function () {
   this.on('event', this.handleEvent.bind(this));
};

Первый аргумент, переданный методу, служит в качестве this в рамках функции, которую возвращает bind. Каждый следующий после первого параметра bind добавляется в начало списка параметров при вызове привязанной функции.

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

var add = function (a, b) {
   return a + b;
};
var add2 = add.bind(null, 2);

add2(10) === 12;

Функторы

Функтор – это любой класс или тип данных, который сохраняет значение и реализует метод map.

Например, Array – это функтор, потому что массив сохраняет значение и реализует метод map, что позволяет нам применять функцию к значениям, которые он хранит.

Подробнее о функторах можно прочитать в предыдущей статьи – Функциональное программирование. Основные техники функционального программирования. Часть 1.

Монада

Монада – это подтип функторов, так как у них есть метод map, но они также реализуют другие методы, например, ap, of, chain. Подробнее о монадах можно прочитать в предыдущей статьи – Функциональное программирование. Основные техники функционального программирования. Часть 1.

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

Повышение надежности кода
Привлекательная сторона вычислений без состояния – повышение надежности кода за счет четкой структуризации и отсутствии необходимости отслеживания побочных эффектов. Любая функция работает только с локальными данными и работает с ними всегда одинаково, независимо от того, где, как и при каких обстоятельствах она вызывается. Невозможность мутации данных при использовании их в разных местах программы исключает появление ошибок которых трудно найти (таких, например, как случайное присваивание неверного значения глобальной переменной в императивной программе).
Удобство организации модульного тестирования
Поскольку функция в функциональном программировании не может порождать побочные эффекты, изменять объекты нельзя как внутри области видимости, так и снаружи (в отличие от императивных программ, где одна функция может установить какую-то внешнюю переменную, считывается второй функцией). Единственным эффектом от вычисления функции является возвращаемой ею результат, и единственный фактор, который влияет на результат – это значения аргументов.
Таким образом, есть возможность протестировать каждую функцию в программе, просто вычислив её с различными наборами значений аргументов. При этом можно не беспокоиться ни о вызове функций в правильном порядке, ни о правильном формировании внешнего состояния. Если любая функция в программе проходит модульные тесты, то можно быть уверенным в качестве всей программы. В императивных программах проверка возвращаемого значения недостаточна: функция может модифицировать внешнее состояние, которое тоже нужно проверять, чего не нужно делать в функциональных программаx.

Материалы для изучение ФП:

Функциональное программирование должно стать вашим приоритетом №1

Paradigms of Computer Programming – Fundamentals

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

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

Master the JavaScript Interview: What is Functional Programming?

Функциональное программирование. Основные техники функционального программирования. Часть 1.

Схожі статті