Перевод статьи Charles Scalfani: So You Want to be a Functional Programmer (Part 4) с наилучшими пожеланиями от автора.
Первый шаг к пониманию идей функционального программирования – самый важный и иногда самый сложный шаг. Но с правильным подходом никаких трудностей быть не должно.
Предыдущие части: Часть 1, Часть 2, Часть 3.
Как вы помните из Части 3, причиной проблемы, из-за которой нам не удавалось скомпоновать функции mult5
и add
, является тот факт, что mult5
принимает один параметр, а add
— целых два.
Мы можем очень легко решить эту проблему, уменьшив количество входных данных до одного для всех функций.
Поверьте мне. Это не так плохо, как звучит.
Мы просто пишем функцию сложения, использующую два входных параметра, но принимающую один за раз. Каррированные функции позволяют нам сделать это.
Каррированная функция — это функция, принимающая один аргумент за раз.
С их помощью мы передадим в add
первый параметр перед тем, как скомпонуем её с mult5
. Затем, когда mult5AfterAdd10
будет вызвана, add
получит свой второй параметр.
В JavaScript мы можем реализовать эту идею, переписав add
:
var add = x => y => x + y;
Этот вариант add
— функция, принимающая один параметр сразу и второй — позже.
Более детально, функция add
принимает отдельный параметр, x
, и возвращает функцию, принимающую следующий отдельный параметр, y
, который, в конечном счёте, будет возвращать результат сложения x
и y
.
Теперь мы можем использовать новый add
, чтобы написать исправный вариант mult5AfterAdd10
:
var compose = (f, g) => x => f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));
Функция компоновки (compose
) получает на вход два параметра: f
и g
. После чего она возвращает функцию, принимающую один параметр, x
, с вызовом которой композиция функций f
после g
осуществится с аргументом x
.
Так что же мы на самом деле сделали? Что ж, мы конвертировали нашу простую старую функцию add
в её каррированный вариант. Это сделало add
более гибкой, поскольку первый параметр, 10
, может быть передан перед непосредственным выполнением функции, а второй — когда mult5AfterAdd10
будет вызвана.
Здесь вам, наверное, должно быть интересно, как же переписать функцию сложения для Elm. Оказывается, делать этого не нужно. В Elm и в других языках функционального программирования все функции автоматически каррированные.
Так что функция add
остаётся неизменной:
add x y =
x + y
А вот как должна была быть написана mult5AfterAdd10
, возвращаясь к Части 3:
mult5AfterAdd10 =
(mult5 << add 10)
Говоря о синтаксисе, Elm одерживает верх над такими императивными языками, как JavaScript, поскольку он изначально оптимизирован для различных задач функционального программирования, например, каррирования или композиции функций.
Другой случай, когда каррирование способно показать себя во всей красе — процесс рефакторинга, во время которого вы создаёте универсальный вариант функции со множеством параметров, а потом используете её для создания более адаптированного варианта, но уже с меньшим количеством входных данных.
Допустим, для примера, что у нас есть следующие функции, обрамляющие строку одинарными и двойными скобками:
bracket str =
"{" ++ str ++ "}"
doubleBracket str =
"{{" ++ str ++ "}}"
И вот, как мы их используем:
bracketedJoe =
bracket "Joe"
doubleBracketedJoe =
doubleBracket "Joe"
Мы можем обобщить bracket
и doubleBracket
:
generalBracket prefix str suffix =
prefix ++ str ++ suffix
Но теперь при каждом вызове generalBracket
мы должны передавать сами скобки входными значениями:
bracketedJoe =
generalBracket "{" "Joe" "}"
doubleBracketedJoe =
generalBracket "{{" "Joe" "}}"
Мы же в действительности хотим взять лучшее из обоих миров.
Если мы перегруппируем входные параметры в generalBracket
, мы сможем создать bracket
и doubleBracket
, выгодно используя факт каррированных функций:
generalBracket prefix suffix str =
prefix ++ str ++ suffix
bracket =
generalBracket "{" "}"
doubleBracket =
generalBracket "{{" "}}"
Заметьте, что располагая статические параметры первыми, то есть prefix
и suffix
, а изменяемые параметры — последними, то есть str
, мы можем легко создавать адаптированные варианты generalBracket
.
Порядок входных параметров очень важен для наиболее выгодного использования каррирования.
Кроме того заметьте, что функции bracket
и doubleBracket
написаны в бесточечном стиле, то есть аргумент str
только предполагается. Обе функции — bracket
и doubleBracket
— ожидают свой последний параметр.
Теперь мы можем использовать их так, как и хотели:
bracketedJoe =
bracket "Joe"
doubleBracketedJoe =
doubleBracket "Joe"
Но только теперь мы используем общую функцию каррирования - generalBracket
.
Давайте рассмотрим три стандартные функции, использующиеся в языках функционального программирования.
Но для начала обратим внимание на следующий JavaScript-код:
for (var i = 0; i < something.length; ++i) {
// do stuff
}
Этот код содержит одну существенную вредную особенность. И это не ошибка. Проблема в том, что это шаблонный код, то есть код, использующийся снова и снова.
Если вы пишете на императивном языке, как Java, C#, JavaScript, PHP, Python и так далее, вы найдете у себя этот шаблон повторяющимся чаще, чем любой другой.
Именно это с этим кодом и не так.
Так избавимся же от него. Давайте обернём его в функцию (или несколько функций) и никогда больше не будет писать цикл for
снова. Ну, то есть почти никогда; по крайней мере, до тех пор, пока полностью не перейдем на функциональное программирование.
var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
things[i] = things[i] * 10; // ТРЕВОГА: ИЗМЕНЕНИЕ ДАННЫХ !!!!
}
console.log(things); // [10, 20, 30, 40]
Вот чёрт! Изменяемость!
Попробуем-ка ещё раз. В этот раз мы не будем изменять things
:
var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]
Так, ладно, мы не изменили things
, но технически мы изменили newThings
. Пока что пропустим это мимо глаз. Всё-таки мы в мире JavaScript. Однажды коснувшись идей функционального программирования, нам больше не хочется прибегать к изменяемости.
На данном этапе важно понять, как эти функции работают и помогают нам уменьшить «шум» в нашем коде.
Давайте возьмём этот код и положим его в функцию. Мы назовём нашу первую стандартную функцию map (прим. пер., английский глагол «наносить на карту»), так как она переносит каждое значение из старого массива в новое значение в новом массиве:
var map = (f, array) => {
var newArray = [];
for (var i = 0; i < array.length; ++i) {
newArray[i] = f(array[i]);
}
return newArray;
};
Обратите внимание, что функция, f
, передаётся вовнутрь, и это позволяет нашей функции map
делать всё, что нам захочется с каждым элементом массива.
Теперь мы можем переписать предыдущий код, используя map
:
var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);
«Мама, мама, посмотри, я написал код без цикла for
!». Теперь его легче читать и, следовательно, анализировать.
Что ж, технически, цикл for
всё ещё есть в функции map
. Но зато теперь мы свободны от постоянного повторения этого шаблонного кода.
Теперь давайте напишем другую стандартную функцию, фильтрующую объекты в массиве:
var filter = (pred, array) => {
var newArray = [];
for (var i = 0; i < array.length; ++i) {
if (pred(array[i]))
newArray[newArray.length] = array[i];
}
return newArray;
};
Заметьте, что если функция-предикат, pred
, возвращает TRUE, мы сохраняем элемент, а если FALSE — выбрасываем его.
Вот как можно применить filter
для фильтрации нечётных чисел:
var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]
Использовать наш новый filter
гораздо проще, чем вручную постоянно переписывать его с помощью цикла for
.
Последняя стандартная функция называется reduce (прим. пер., английский глагол «уменьшать»). Как правило, она используется, когда надо взять список и свести его к одному значению, но, на самом деле, её возможности куда шире.
Обычно в функциональных языках эта функция носит название fold (прим. пер., английский глагол «свёртывать» или «складывать»).
var reduce = (f, start, array) => {
var acc = start;
for (var i = 0; i < array.length; ++i)
acc = f(array[i], acc); // f() принимает 2 аргумента
return acc;
});
Функция reduce
принимает функцию свёртки, f
, исходное значение, start
, и array
.
Имейте в виду, что функция свёртки, f
, принимает два параметра: текущий элемент массива array
и аккумулятор acc
. Она будет использовать эти параметры для обновления аккумулятора каждую новую итерацию. Значение аккумулятора в момент последней итерации будет возвращено из функции.
Вот пример, который поможет нам понять, как это работает:
var add = (x, y) => x + y;
var values = [1, 2, 3, 4, 5];
var sumOfValues = reduce(add, 0, values);
console.log(sumOfValues); // 15
Заметьте, что функция add
принимает два параметра и складывает их. Наша функция reduce
как раз ожидает функцию, принимающую два параметра, так что они хорошо сработаются вместе.
Мы начинаем со значения start
, равного нулю, и шаг за шагом суммируем значения нашего массива values
. С каждой новой итерацией сумма внутри функции reduce
увеличивается. И, в конце концов, накопленное значение возвращается как sumOfValues
.
Каждая из этих функций, map
, filter
и reduce
, упрощает нам работу с массивами и освобождает от скучного повторения шаблонных циклов for
.
Но в функциональном программировании они ещё более ценны, поскольку в случаях, когда требуется прибегнуть к цикличности, сложно ограничиться одними рекурсиями. Итеративные функции не просто чрезвычайно полезны. Они просто необходимы.
Пока что достаточно.
В последующих частях этой статьи я расскажу про порядок выполнения, прозрачность ссылок, типы и ещё кое о чём.
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.