Итак, вы хотите научиться функциональному программированию (Часть 4)
Первый шаг к пониманию идей функционального программирования — самый важный и иногда самый сложный шаг. Но с правильным подходом никаких трудностей быть не должно.
Каррирование
Как вы помните из Части 3, причиной проблемы, из-за которой нам не удавалось скомпоновать функции mult5 и add , является тот факт, что mult5 принимает один параметр, а add — целых два.
Мы можем очень легко решить эту проблему, уменьшив количество входных данных до одного для всех функций.
Поверьте мне. Это не так плохо, как звучит.
Мы просто пишем функцию сложения, использующую два входных параметра, но принимающую один за раз. Каррированные функции позволяют нам сделать это.
Каррированная функция— это функция, принимающая один аргумент за раз.
С их помощью мы передадим в add первый параметр перед тем, как скомпонуем её с mult5 . Затем, когда mult5AfterAdd10 будет вызвана, add получит свой второй параметр.
В JavaScript мы можем реализовать эту идею, переписав add :
Этот вариант add — функция, принимающая один параметр сразу и второй — позже.
Более детально, функция add принимает отдельный параметр, x , и возвращает функцию, принимающую следующий отдельный параметр, y , который, в конечном счёте, будет возвращать результат сложения x и y .
Теперь мы можем использовать новый add , чтобы написать исправный вариант mult5AfterAdd10 :
Функция компоновки ( compose ) получает на вход два параметра: f и g . После чего она возвращает функцию, принимающую один параметр, x , с вызовом которой композиция функций f после g осуществится с аргументом x .
Так что же мы на самом деле сделали? Что ж, мы конвертировали нашу простую старую функцию add в её каррированный вариант. Это сделало add более гибкой, поскольку первый параметр, 10 , может быть передан перед непосредственным выполнением функции, а второй — когда mult5AfterAdd10 будет вызвана.
Здесь вам, наверное, должно быть интересно, как же переписать функцию сложения для Elm. Оказывается, делать этого не нужно. В Elm и в других языках функционального программирования все функции автоматически каррированные.
Так что функция add остаётся неизменной:
А вот как должна была быть написана mult5AfterAdd10 , возвращаясь к Части 3:
Говоря о синтаксисе, Elm одерживает верх над такими императивными языками, как JavaScript, поскольку он изначально оптимизирован для различных задач функционального программирования, например, каррирования или композиции функций.
Каррирование и рефакторинг
Другой случай, когда каррирование способно показать себя во всей красе — процесс рефакторинга, во время которого вы создаёте универсальный вариант функции со множеством параметров, а потом используете её для создания более адаптированного варианта, но уже с меньшим количеством входных данных.
Допустим, для примера, что у нас есть следующие функции, обрамляющие строку одинарными и двойными скобками:
И вот, как мы их используем:
Мы можем обобщить bracket и doubleBracket :
Но теперь при каждом вызове generalBracket мы должны передавать сами скобки входными значениями:
Мы же в действительности хотим взять лучшее из обоих миров.
Если мы перегруппируем входные параметры в generalBracket , мы сможем создать bracket и doubleBracket , выгодно используя факт каррированных функций:
Заметьте, что располагая статические параметры первыми, то есть prefix и suffix , а изменяемые параметры — последними, то есть str , мы можем легко создавать адаптированные варианты generalBracket .
Порядок входных параметров очень важен для наиболее выгодного использования каррирования.
Кроме того заметьте, что функции bracket и doubleBracket написаны в бесточечном стиле, то есть аргумент str только предполагается. Обе функции — bracket и doubleBracket — ожидают свой последний параметр.
Теперь мы можем использовать их так, как и хотели:
Но только теперь мы используем общую функцию каррирования — generalBracket .
Стандартные функции функционального программирования
Давайте рассмотрим три стандартные функции, использующиеся в языках функционального программирования.
Но для начала обратим внимание на следующий JavaScript-код:
Этот код содержит одну существенную вредную особенность. И это не ошибка. Проблема в том, что это шаблонный код, то есть код, использующийся снова и снова.
Если вы пишете на императивном языке, как Java, C#, JavaScript, PHP, Python и так далее, вы найдете у себя этот шаблон повторяющимся чаще, чем любой другой.
Именно это с этим кодом и не так.
Так избавимся же от него. Давайте обернём его в функцию (или несколько функций) и никогда больше не будет писать цикл for снова. Ну, то есть почти никогда; по крайней мере, до тех пор, пока полностью не перейдем на функциональное программирование.
Вот чёрт! Изменяемость!
Попробуем-ка ещё раз. В этот раз мы не будем изменять things :
Так, ладно, мы не изменили things , но технически мы изменили newThings . Пока что пропустим это мимо глаз. Всё-таки мы в мире JavaScript. Однажды коснувшись идей функционального программирования, нам больше не хочется прибегать к изменяемости.
На данном этапе важно понять, как эти функции работают и помогают нам уменьшить «шум» в нашем коде.
Давайте возьмём этот код и положим его в функцию. Мы назовём нашу первую стандартную функцию map (прим. пер., английский глагол «наносить на карту»), так как она переносит каждое значение из старого массива в новое значение в новом массиве:
Обратите внимание, что функция, f , передаётся вовнутрь, и это позволяет нашей функции map делать всё, что нам захочется с каждым элементом массива.
Теперь мы можем переписать предыдущий код, используя map :
«Мама, мама, посмотри, я написал код без цикла for !». Теперь его легче читать и, следовательно, анализировать.
Что ж, технически, цикл for всё ещё есть в функции map . Но зато теперь мы свободны от постоянного повторения этого шаблонный кода.
Теперь давайте напишем другую стандартную функцию, фильтрующую объекты в массиве:
Заметьте, что если функция-предикат, pred , возвращает TRUE, мы сохраняем элемент, а если FALSE — выбрасываем его.
Вот как можно применить filter для фильтрации нечётных чисел:
Использовать наш новый filter гораздо проще, чем вручную постоянно переписывать его с помощью цикла for .
Последняя стандартная функция называется reduce (прим. пер., английский глагол «уменьшать»). Как правило, она используется, когда надо взять список и свести его к одному значению, но, на самом деле, её возможности куда шире.
Обычно в функциональных языках эта функция носит название fold (прим. пер., английский глагол «свёртывать» или «складывать»).
Функция reduce принимает функцию свёртки, f , исходное значение, start , и array .
Имейте в виду, что функция свёртки, f , принимает два параметра: текущий элемент массива array и аккумулятор acc . Она будет использовать эти параметры для обновления аккумулятора каждую новую итерацию. Значение аккумулятора в момент последней итерации будет возвращено из функции.
Вот пример, который поможет нам понять, как это работает:
Заметьте, что функция add принимает два параметра и складывает их. Наша функция reduce как раз ожидает функцию, принимающую два параметра, так что они хорошо сработаются вместе.
Мы начинаем со значения start , равного нулю, и шаг за шагом суммируем значения нашего массива values . С каждой новой итерацией сумма внутри функции reduce увеличивается. И, в конце концов, накопленное значение возвращается как sumOfValues .
Каждая из этих функций, map , filter и reduce , упрощает нам работу с массивами и освобождает от скучного повторения шаблонных циклов for .
Но в функциональном программировании они ещё более ценны, поскольку в случаях, когда требуется прибегнуть к цикличности, сложно ограничиться одними рекурсиями. Итеративные функции не просто чрезвычайно полезны. Они просто необходимы.
Мой мозг.
Пока что достаточно.
В последующих частях этой статьи я расскажу про порядок выполнения, прозрачность ссылок, типы и ещё кое о чём.