Каррирование и функции высшего порядка – это основы функционального программирования. JavaScript поддерживает их "из коробки", а значит, мы просто обязаны использовать эти возможности для создания чистого высокопроизводительного кода, который легко переиспользовать.
Шаг 1. Функции первого класса
Одна из важных особенностей функционального программирования на JavaScript основана на том, что функции в языке являются "гражданами первого класса" (first-class citizens). Это означает, что вы можете делать с ними все, что придет в голову, например, передавать их как аргументы другим функциями или возвращать из других функций.
firstClassType
– это пример функции идентичности, которая принимает некоторое значение в качестве аргумента и его же возвращает, не изменяя.
myFunctionAsOutput
не принимает никаких параметров, а просто возвращает переменную firstClassType
, которая содержит функцию.
По сути вызов myFunctionAsOutput()
– это то же самое, что и прямое обращение к firstClassType
– эти выражения взаимозаменяемы. Поэтому мы можем вызвать результат работы myFunctionAsOutput()
как обычную функцию с помощью круглых скобок.
Следующий фрагмент делает то же самое, но более многословно.
Посмотрим на еще один пример, в котором функция выступает как параметр другой функции:
Функция myFunctionAsInputAndOutput
принимает функцию и ее же возвращает. По сути это просто еще один пример функции идентичности. То есть вызов myFunctionAsInputAndOutput(firstClassType)
– это то же самое, что и прямое обращение к firstClassType
, значит, мы тоже можем его вызвать.
То же самое можно сделать и с firstClassType
, если в качестве параметра input
передать функцию.
Выражение firstClassType(firstClassType)
возвращает саму функцию firstClassType
, которую вновь можно вызвать :)
Единственная разница между двумя функциями идентичности firstClassType
и myFunctionAsInputAndOutput
состоит в том, что первая – это стрелочная функция, а вторая – обычная.
Шаг 2. Функции высшего порядка
Таким образом, все функции из предыдущего примера – это функции высшего порядка.
Вложенные функции (одна функция создается внутри другой) всегда имеют доступ к области видимости родительской функции, которая невидима снаружи. Таким образом, функция возвращенная из другой функции, может работать с данными, которые больше никому не видны. Это называется замыканием.
Это упрощенная реализация функции мемоизации. Давайте разбираться, что здесь происходит.
Например, у нас есть функция, которая приводит строку к верхнему регистру:
Мы хотим мемоизировать ее (предположим, что изменение регистра – это очень трудозатратная операция). Мы передаем эту функцию в функцию высшего порядка memo
и получаем взамен другую функцию, которую сохраняем в переменную upperCase
.
Новая функция работает точно так же, как и старая: она принимает строку и возвращает ту же строку в верхнем регистре. Но под капотом используется техника мемоизации. Если входные данные повторяются, вычисление не происходит заново. Вместо этого берется ранее сохраненное значение из кеша memory
.
Шаг 3. Каррирование функций в JavaScript
Вам тоже кажется, что каррирование – это что-то из кулинарии? :)
На первом шаге мы уже составляли сложные выражения с несколькими круглыми скобками для последовательного вызова функций. Там одна функция возвращала другую, которую мы не записывали в переменную, а сразу же вызывали.
Каррирование выглядит точно так же:
Функции curry1
и curry2
записаны по-разному, но работают абсолютно одинаково. Это сделано для наглядности, чтобы вы не запутались в стрелочных функциях.
Каждая стрелка в curry2
возвращает выражение, которое стоит справа от нее. Первая стрелка (первый вызов функции) возвращает вложенную функцию:
При этом используется входящий аргумент первой функции – a
.
Второй вызов (вторые круглые скобки) вернет уже результат выполнения вложенной функции:
Каррирование – это способ пошагового получения последовательности аргументов.
В curry2
всего два параметра (и две стрелки), но вы можете использовать сколько угодно при необходимости (только не забывайте о читаемости кода).
В функциональном программировании предпочтительно создавать унарные функции, которые имеют всего один аргумент. Из них уже составляются функции с большей арностью. Например, бинарная функция curriedBinary
составлена из двух унарных.
Шаг 4. Переиспользование каррированных функций
Каррированные функции очень удобны, так как могут быть легко переиспользованы в разных комбинациях для создания других функций.
В этом примере мы используем три унарные функции filter
, lowerCaseOf
и includes
. Из них мы собираем сложные функции isTortoise
и filterTortoises
, как конструктор Lego из отдельных деталек.
Шаг 5. Улучшенная композиция функций
Каррировать JavaScript-функции при каждом использовании – занятие раздражающее, поэтому лучше использовать уже готовую библиотеку со множеством утилит, например, @7urtle/lambda. Помимо прочего она предоставляет полезную функцию compose
для удобной композиции нескольких функций.
Вы передаете ей несколько функций в качестве аргументов, через запятую, а она выстраивает из них конвейер. При этом выполнение функций будет осуществляться справа налево, то есть функция, которую вы передадите последней, выполнится первой, а ее результат будет использован предпоследней функцией.
Обратите внимание, как изящно и коротко (всего в одной строчке кода) нам удалось определить функцию isTortoise
, которая на самом деле выполняет большой объем работы. И все это практически без потери читаемости.
Альтернативный способ сделать то же самое – функция imperativeFilterTortoises
.
Она написана в императивном стиле и занимает целых 17 строк (515 символов против 115 у декларативного варианта)!
Бонус: функции curry и nary
Вероятно, вы уже задумались о поиске или создании волшебной функции, которая автоматически каррирует вашу n-арную функцию, или позволит вызывать каррированную функцию в привычном стиле с несколькими аргументами.
Тогда вам нужны функции curry
и nary
. Первая принимает n-арную функцию, вторая – каррированную. Обе позволяют вызывать полученные функции в любом стиле (как каррированные или как n-арные).
Обратите внимание, что сами функции curry
и nary
написаны в декларативном функциональном стиле. Они обе доступны в библиотеке @7urtle/lambda. Фактически, библиотека под капотом использует функцию nary
, чтобы обеспечить возможность вызывать пользовательские функции любым удобным способом.
Познав магию функционального программирования на JavaScript, вы не захотите возвращаться обратно. Это очень простой и элегантный способ создания кода, который нельзя не оценить.
Комментарии