Forever Functional: функции высшего порядка

От автора: общие особенности функционального программирования (FP) включают передачу функций в качестве параметров другим функциям и возвращение в результате новых функций! В строгом теоретическом смысле функция высшего порядка (HOF) принимает одну или несколько функций в качестве аргументов и, может также возвращать функцию в качестве результата.

В этой статье мы рассмотрим HOF, возвращающие новые функции. Мы можем разделить такие HOF на три группы:

Оболочка функционала: исходная функциональность сохранена, но добавлена некоторая новая функция. Мы видели яркие примеры этого в наших статьях о запоминании функций и запоминании промисов. В обоих случаях созданные функции выполняли ту же работу, но более эффективно благодаря кешированию. Другими примерами обертки могут быть время (получение данных о производительности времени) или ведение журнала (создание журналов), мы увидим это ниже.

Изменение функционала: исходная функциональность каким-либо образом модифицируется. Например, у нас может быть логическая функция, которая проверяет условие и изменяет его, чтобы инвертировать его результат. Другой случай: мы можем исправить арность функции, чтобы избежать проблем, которые вы видели в статье о программировании без точек.

Создание функционала: эти HOF предоставляют новые собственные функции. В качестве примера мы можем отделить методы от объектов, чтобы использовать их как общие функции, или преобразовать функцию, которая работает с обратными вызовами в промис для более чистой обработки; мы займемся этим ниже.

Давайте рассмотрим примеры этих преобразований, чтобы вы могли получить представление о многих возможных вариантах использования HOF!

Обертывание оригинальной функциональности

Обертывание функции подразумевает создание новой версии с той же функциональностью, что и исходная, с добавлением некоторой новой функциональности.

Функции времени

Предположим, мы хотим измерить производительность функции. Мы могли бы изменить функцию, чтобы получать текущее время в начале и непосредственно перед возвратом результата, но с HOF нам это не нужно. Мы напишем функцию addTiming(…), которая вернет новую функцию, которая будет регистрировать данные о времени в дополнение к основной функциональности.

Функция, которую мы возвращаем, начинается с получения начального времени ([1]) с помощью performance.now() для большей точности. Затем она вызывает исходную функцию ([2]) и, если нет проблем, записывает «Normal exit», имя функции и время, которое потребовалось. Если функция сгенерировала исключение ([3]), она регистрирует «Exception thrown», плюс имя функции и общее время, и снова выбрасывает то же исключение для дальнейшего процесса. Посмотрим на пример.

Мы меняем нашу исходную функцию на новую версию, которая включает время ([1]). Когда мы вызываем эту новую функцию ([2]), мы получаем некоторый журнал в консоли, и возвращаемое значение — это то, что рассчитала исходная функция… Теперь с помощью HOF мы можем синхронизировать любую функцию, не изменяя ее!

Функции регистрации

Другой распространенный пример упаковки — это добавление функции ведения журнала. Измененная таким образом функция позволит вам увидеть на выводе в консоли, когда она вызывается, с какими аргументами и что она возвращает. Как и в случае с таймингом, мы не хотим изменять исходную функцию — это чревато ошибками и не является хорошей практикой!

Подобно тому, что мы делали в предыдущем разделе, мы напишем функцию addLogging(…), которая будет принимать функцию в качестве параметра и возвращать новую функцию, которая начнет с регистрации ее аргументов, затем вызовет исходную функцию, запишет все, что она вернула, и, наконец, вернет это значение вызывающей стороне. Мы также учтем исключения. Возможная реализация могла быть следующей.

Мы начинаем с регистрации аргументов функции ([1]). Затем мы вызываем исходную функцию и сохраняем ее результат ([2]). Если проблем нет, мы просто регистрируем «Exit» и результат. Если есть какая-то ошибка, мы регистрируем «Error» и исключение. Мы можем просто использовать эту функцию; давайте повторно воспользуемся примером из предыдущего раздела.

Мы создаем новую функцию ([1]), которая будет вести журнал, и когда мы вызываем ее ([2]), мы получаем дополнительный вывод. Кстати, вы можете добавить и логирование, и тайминг, например: addTiming(addLogging(add3)).

Изменение исходной функциональности

Во второй категории изменений мы увидим две простые ситуации: изменение логического решения для получения гибкости в таких функциях, как фильтрация, и изменение арности функций для решения проблемы с функциями, которые ожидают необязательные параметры.

Отрицание условия

Предположим, вы написали функцию для проверки некоторого условия. Например, у вас может быть функция для фильтрации учетных записей по некоторым внутренним критериям. С помощью этой функции вы можете написать следующий код.

Этот код использует функцию isGoodAccount(…) для извлечения правильных учетных записей из заданного списка. Итак, что бы вы сделали, если бы вместо этого вам нужно было извлечь неверные учетные записи? Вы, конечно, могли бы написать что-то подобное, но это выглядит не так красиво.

Мы можем получить лучшее решение, используя HOF, которая будет инвертировать (отрицать) все, что производит данная функция. Запишем эту HOF в одну строку следующим образом.

Теперь вы можете написать следующее.

Теперь код такой же разборчивый, как и код для фильтрации правильных аккаунтов! Вы можете расширить идею, чтобы разрешить объединение нескольких условий с помощью логических операторов: например, вы можете написать HOF and(…) и or(…), которые позволили бы вам написать что-то вроде этого:

И, конечно, можно все объединить!

Изменение арности функций

В статье о безточечном стиле мы видели проблему.

Почему вторая карта (…) дала такие странные результаты? Причина в том, что parseInt(…) позволяет использовать второй (необязательный) аргумент, а parseFloat(…) не позволяет. Технически арность этих функций равна 2 и 1 соответственно. Мы можем очень просто преобразовать первую функцию в унарную (арность 1), используя HOF.

unary(fn) создает новую функцию, которая, учитывая несколько аргументов, вызывает только fn, отбрасывая остальные. Благодаря этому наша проблема легко решается другим способом, нежели тем, что мы видели в нашей предыдущей статье.

Таким же образом, как мы трансформировали функцию в унарную, было бы просто писать binary(…), ternary(…), для преобразования функций арности 2, 3 и т.д .; оставим это как еще одно упражнение!

Создание нового функционала

Рассмотрим пару примеров: преобразуем методы в функции, а функции использующие обратный вызов — в промисы.

От методов к функциям

Некоторые методы (например map(…)) доступны для массивов, но если вы захотите использовать их в другом месте, будет проблемма. Однако мы можем написать HOF, который преобразует любой метод в эквивалентную функцию. Вместо object.method(args), вы можете написать method(object,args) — и теперь у вас есть функция, которую вы можете передать в истинном стиле FP!

Как мы можем с этим справиться? Ключевым является метод bind(…):

(Кстати, есть и другие способы реализации demethodize(…), например, с помощью apply(…) или call(…) — если вы готовы принять вызов, попробуйте сделать это!). Допустим, вы хотели использовать метод .toUpperCase(…) как функцию. Вы бы написали следующее.

FP больше ориентирован на функции, чем на методы, поэтому возможность преобразовывать методы в функции помогает вам работать лучше.

От обратных вызовов к промисам

Давайте рассмотрим пример из Node. В нем, по определению, все асинхронные функции требуют обратного вызова «error first», например (err, data) => { … }. Если err имеет значение null, предполагается, что операция завершилась успешно и data получает свой результат; в противном случае err указывает причину ошибки. Однако вместо этого мы могли бы предпочесть работу с промисами. Мы можем написать HOF, которая преобразует асинхронную функцию, требующую обратного вызова, в промис, который позволяет использовать .then/.catch или await. (Хорошо, в Node уже есть util.promisify(), чтобы делать именно это, но давайте посмотрим, как мы это сделаем.) Необходимое преобразование несложно.

Для любой функции мы вернем новую, которая также вернет функцию. После вызова исходной функции промис будет разрешен или отклонен в зависимости от того, что было возвращено. Мы можем написать так:

Теперь, как нам прочитать файл в Node с помощью функции fs.readFile(…)? (Да, Node также предоставляет API fs/promises, который уже возвращает промисы. В реальном производстве мы бы использовали это вместо того, чтобы писать промис самостоятельно.) Мы можем сделать следующее:

Легко и приятно!

Резюме

В этой статье мы исследовали концепцию функций высшего порядка (HOF), общую особенность функционального программирования, и увидели несколько примеров их использования для удовлетворения общих повседневных потребностей в разработке. Использование HOF даст вам большую свободу действий при написании более короткого, ясного и эффективного кода; попробуйте попрактиковаться с HOF!

Автор: Federico Kereki

Источник: blog.openreplay.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен

Метки:

Похожие статьи:

Комментарии Вконтакте:

Комментарии запрещены.