Функциональное программирование стало горячей темой в мире 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.