Объяснение азов функционального программирования на простых примерах для создания лаконичного и легко поддерживаемого кода.
В последние годы отмечается еще большее разграничение между традиционным и функциональным программированием в JavaScript. Суть его не в превосходстве одного над другим, а в личных предпочтениях.
Данная статья рассчитана на новичков, постигающих азы функционального программирования.
Что такое функциональное программирование в теории?
- Чистый код – программу легко читать и поддерживать.
- Синтаксическая эффективность – проблема, которую пытается решить код, выражается меньшим количеством строк без вреда для функционала.
- Большая абстрактность – объемная логика для цикла
for
заменяется большей компактностью, можно даже сказать, символичностью. Код становится похож на математическое уравнение. - Меньше ошибок – если научиться мыслить категориями функционального программирования, то можно оградить себя от попадания в череду самых распространенных ошибок и ловушек.
- Больше дела – меньше строк – такой код выглядит короче, чем его аналог из парадигмы традиционного программирования.
Конечно же, с функциональным программированием вам придется пересмотреть свое видение кода. Данная концепция активно используется разработчиками в проектах, для которых эффективность и надежность кода стоят на первом месте.
Что такое функциональное программирование на практике?
Взгляните на простой цикл for
:
let LIST = [1, 2, 3]; // Часть 1: Цикл for в роли итератора и инкрементора for (let i= 0; i< LIST.length; i++) { // Часть 2: выражение оператора для каждой итерации LIST[i] = LIST[i] + 1; } console.log(LIST); // [2, 3, 4]
Вы берете список и прогоняете его через двойной перебор (итерацию) элементов массива. Затем увеличиваете каждое значение на единицу. Таким образом, начальное значение [1,2,3]
превращается в [2,3,4]
.
Проще простого, даже гуглить не придется.
Этот пример как нельзя лучше иллюстрирует традиционное использование знаменитого цикла for
в качестве итератора.
Чистые функции
Говоря о функциональном программировании, нельзя не упомянуть о чистых функциях. Так что такое чистая функция?
Основное назначение чистой функции – убрать побочные эффекты.
Побочный эффект возникает в функциях, которые возвращают неожиданные результаты, либо оказывают иное влияние на параметры функции, изменяя состояние системы.
При изменении состояния системы возникает угроза целостности программы и снижается качество кода. Одна ошибка ведет к появлению другой. И вот вы уже не понимаете, откуда берутся эти баги. Никому не пожелаешь такого.
Но… чистая функция – это не свойство какого-то одного языка. Ее придумали для обозначения функции, в которой при заданном наборе аргументов всегда возвращаются предсказуемые результаты. Например, 1+2 всегда вернет 3.
С другой стороны, есть Math.random()
. Она никак не может сойти за чистую функцию, т. к. каждый раз возвращает разные числовые значения.
Чистота функции часто определяется возвращаемым значением.
Если функция возвращает неожиданное значение даже при одинаковых аргументах, то она называется нечистой функцией (функцией с побочными эффектами).
/ Чистая функция – возвращает одинаковое значение для одних и тех же наборов аргументов function sum(a, b) { return a + b; } // Нечистая функция – возвращает случайные значения вне зависимости от передаваемых аргументов function sum(a, b) { return Math.random() * (a + b); }
Можете ли вы писать, в основном, чистые функции? Если так, то вы на шаг ближе к освоению азов функционального программирования.
Сначала чистота функции кажется чем-то малопонятным, особенно без наглядных примеров. Однако компьютерная программа – это нечто большее, чем простой набор операторов. И то, что вы прописываете внутри функций, может затронуть общее состояние программы.
Всегда помните о главном: никто не может писать 100% чистые программы. Но именно чистота функций позволяет избегать многих распространенных ошибок.
Дальше все станет понятнее. Вы увидите, как можно сочетать чистые функции с другими принципами функционального программирования.
Перезапись цикла for
для функционального программирования
Для перезаписи цикла for
будет использоваться метод массива map
(сопоставление).
let LIST = [1, 2, 3]; LIST.map( function(item) { return item + 1; } );
В EcmaScript 6 с поддержкой парадигмы функционального программирования можно взять стрелочные функции. Стрелочная функция еще больше сокращает функции в коде:
LIST.map( (item) => { return item + 1; } );
А при удалении return
функция принимает следующий вид:
LIST.map( (item) => item + 1 );
Работает также, но выглядит короче и "функциональнее". В данном случае функциональность не говорит о том, что надо пользоваться только функциями JavaScript. Суть в другом: лаконичный код больше похож на математическое уравнение.
Стрелочные функции возвращают значение внутри {}
, выраженное одним оператором. Причем данную функцию можно сократить еще больше, если удалить ()
:
LIST.map( item => item + 1 );
Помните первоначальный цикл for
? Это он и есть, только с записью в одном выражении.
Хотя, может, это «почти что он»?
Еще один важный нюанс функционального программирования: стрелочные функции скрывают свою область значений. То есть в функцию не добавляют исходные значения массива! В итоге так и останется [1,2,3]
.
А метод map
приводит к следующему:
let LIST = [1,2,3]; LIST.map( item => { item + 1 }); console.log(LIST); // по-прежнему: [1,2,3], ничего не инкрементировано!
Давайте вынесем функцию из метода map
и сохраним ее отдельно:
let LIST = [1,2,3]; let add = item => item + 1; LIST.map( add ); console.log(LIST); // опять: [1,2,3], ничего не инкрементировалось!
Так функция выглядит немного чище. Тем не менее, значение списка все еще не увеличилось (не инкрементировалось).
Это и есть смена парадигмы. Вам еще предстоит разобраться в причинах, почему результат остается прежним.
Инкрементация списка отсутствует только потому, что map
не изменяет сам список. Он возвращает копию списка с применением обратного вызова каждого из элементов. А для получения инкрементации переменной нужно присвоить LIST.map(add)
:
let copy = LIST.map(add);
Теперь в копии списка хранится инкрементированное значение. Но… это уже не тот массив, с которым вы работали до этого. Опять же – очередной плюс функционального программирования.
Помните, как в начале статьи речь шла о побочных эффектах? Стрелочная функция скрывает собственную область значений, поэтому ей не нужно просачиваться в глобальную область видимости или менять значения начального массива. В рамках функционального программирования – это хорошо. И вполне предсказуемо.
Чистота функции во всей красе! И да, она немного ограничивает возможности разработчика. Но такой код выглядит менее громоздким, а разработчик получает копию начального массива.
Скрытые области функционального программирования
Функциональное программирование – это не просто преобразование цикла for
к более лаконичным выражениям. Область применения здесь намного шире. Пример: код выше, в котором функции map
и arrow
скрывают элементы в своей области действия.
Массив содержит целый набор методов, характерных для функционального программирования.
Один из них называется reduce
(сокращение). При соединении map
с reduce
можно изменять элементы массива без вынесения их в глобальную область видимости. Вся логика программы дальше (отличная практика в разработке).
let LIST = [1,2,3]; let add = item => { return item + 1 }; let sum = (A, I) => { return A + I }; let val = LIST.map(add).reduce(sum, 0); console.log( val ); // 9
Кроме того, можно смело удалять return
и {}
без вреда для функционала:
let LIST = [1,2,3]; let add = item => item + 1; let sum = (A, I) => A + I; let val = LIST.map(add).reduce(sum, 0); console.log( val ); // 9
Опять же, такая структура чем-то похожа на математическое уравнение (функцию), то есть на функциональное программирование. Однако следует четко понимать, что функциональное программирование не есть функции JavaScript. И по мере разрастания первого, второе постепенно сходит на нет.
Редукторы – это функции, которые сокращают результат по определенному правилу. Программист сам задает это правило. Например, правило может выражаться в функции суммирования sum: A + I
, что расшифровывается как Accumulator + Item
. По сути, именно это и делает цикл for
.
Аккумулятор всегда присутствует в операции reduce
(сокращения). Он отслеживает совокупный эффект операции.
Второй параметр reduce
приравнивается к 0
. Поэтому аккумулятор также равен 0
.
Таким образом, сам цикл начинается с нулевой отметки на счетчике. После выполнения кода аккумулятору присваивается возвращаемое значение (оно хранится в переменной val
).
По мере итерации кода и суммирования функций происходит добавление нового значения.
Результат – 9
.
Раз по условиям 1
добавляется к [1,2,3]
, то получается [2,3,4]
.
Затем при запуске редуктора складываются все значения из [2,3,4]
, и возвращается результат.
В итоге получается: 2 + 3 + 4 = 9
.
Этот пример наглядно показывает небольшую разницу в логике функционального программирования и использования цикла for
.
Функциональное программирование привносит красоту кода и его идеальное выполнение. Функции работают так же четко, как и математическое уравнение.
Перевод статьи JavaScript Teacher: An Introduction to Functional Programming Style in JavaScript
Комментарии