Практическая статья о передаче функций любовного порядка в качестве входов, выходов или параметров.
Функциональное программирование — это парадигма программирования, которая фокусируется на использовании чистых функций, то есть функций, которые зависят только от своих входных аргументов и не имеют побочных эффектов. Одной из ключевых особенностей функционального программирования является возможность передавать функции в виде значений, что обеспечивает большую гибкость и модульность кода.
В этой короткой статье мы обсудим три основных способа передачи функций в функциональном программировании: в качестве входных данных, в качестве вывода и в качестве параметра.
Передача функций в качестве входных данных
Передача функции в качестве входных данных означает, что функция используется в качестве аргумента другой функции. Это обычно используется в функциях более высокого порядка, которые представляют собой функции, которые принимают одну или несколько функций в качестве аргументов или возвращают функцию в качестве результата.
Например, функция фильтра во многих языках программирования принимает функцию в качестве аргумента и применяет эту функцию к каждому элементу в списке, возвращая новый список только с теми элементами, для которых функция возвращает значение true. Это обеспечивает мощный и гибкий способ выполнения операций фильтрации в списках с точными критериями фильтрации, определяемыми функцией, переданной в качестве аргумента.
В качестве примера предположим, что мы хотим отфильтровать список BmiCategories на основе динамических критериев. «filter» или «predicate» будет функцией, которая принимает в качестве входных данных BmiCategory и возвращает логическое значение:
Давайте создадим функцию, которая находит нездоровые категории ИМТ:
public boolean unhealthyCategory(BmiCategory bmi) { return bmi.maxValue() < 20 || bmi.minValue() > 20; } @Test void test() { List<BmiCategory> result = bmiCategories(this::unhealthyCategory); // assert ... }
В этом примере мы наблюдаем силу передачи функции в качестве входных данных для другой функции. Передавая функцию в качестве единственного параметра, мы можем получить осмысленный результат, возвращаемый второй функцией. Смотрим всю схему:
Передача функций в качестве вывода
Передача функции в качестве вывода означает, что функция возвращается как результат другой функции. Это часто используется в функциях, которые генерируют другие функции, такие как фабричные функции.
Рассмотрим функцию расчета ИМТ, которая принимает два параметра: вес и рост.
public Optional<BmiCategory> bmiCategory(double height, double weight) { double bmi = weight / (height * height); return Stream.of( new BmiCategory("underweight", Double.MIN_VALUE, 18.5), new BmiCategory("healthy weight", 18.5, 25.0), new BmiCategory("overweight ", 25.0, 30.0), new BmiCategory("obesity ", 30.0, Double.MAX_VALUE) ) .filter(categ -> categ.minValue() > bmi && categ.maxValue() < bmi) .findAny(); }
Теперь предположим, что мы заранее знаем, что все кандидаты имеют фиксированный рост 2 метра. В таких случаях мы можем создать более общую функцию, которая принимает фиксированный рост в качестве параметра и генерирует другую функцию, которую можно повторно использовать для вычисления ИМТ каждого кандидата с разным весом.
Это практический пример функции, которая возвращает другую функцию, что позволяет динамически создавать функции, которые могут запрашивать различные категории значений ИМТ.
public Function<Double, Optional<BmiCategory>> bmiForFixedHeight(double height) { return weight -> bmiCategory(height, weight); } @Test void test() { var tallPeopleCalculator = bmiForFixedHeight(2.0); var bmi1 = tallPoepleCalculator.apply(76.5); // basically, same as bmiCategory(2.0, 76.5) //or, in a single line: var bmi2 = bmiForFixedHeight(2.0).apply(76.5); }
Этот метод разбиения функций с несколькими аргументами на последовательность функций с одним параметром называется каррированием. Если вам интересно узнать больше об этом и о том, как это можно использовать в Java, я рекомендую вам просмотреть одну из моих предыдущих статей, посвященных этой теме:
Передача функций в качестве аргументов
Передача функции в качестве параметра означает, что функция определена внутри другой функции и может быть вызвана из этой функции. Это обычно используется в функциях обратного вызова, которые вызываются другими функциями при возникновении определенных событий.
Другой распространенный пример — когда мы передаем функцию, которая может изменить выполнение или алгоритм в дополнение к обычным параметрам. Это позволяет использовать более гибкий и динамичный подход к программированию, и в функциональном программировании это эквивалент «шаблона стратегии»:
В предыдущем примере мы предполагали, что аргументы веса и роста измеряются в метрической системе, а килограммы и метры являются соответствующими единицами измерения. Однако давайте предположим, что мы также хотим поддерживать систему США, где вес обычно измеряется в фунтах, а рост — в дюймах. Чтобы получить правильный результат ИМТ, нам нужно умножить результат на коэффициент преобразования 703.
Есть разные способы реализовать это преобразование, но давайте реализуем эту функцию, используя «Шаблон упрощенной стратегии» и передав дополнительную функцию для преобразования результата:
public Optional<BmiCategory> bmiCategory(double height, double weight, Function<Double, Double> convertResult) { double bmi = weight / (height * height); double convertedBmi = convertResult.apply(bmi); return Stream.of( new BmiCategory("underweight", Double.MIN_VALUE, 18.5), new BmiCategory("healthy weight", 18.5, 25.0), new BmiCategory("overweight ", 25.0, 30.0), new BmiCategory("obesity ", 30.0, Double.MAX_VALUE) ) .filter(categ -> categ.minValue() > convertedBmi && categ.maxValue() < convertedBmi) .findAny(); }
Теперь давайте вызовем эту функцию для человека ростом 79 дюймов, весом 167 фунтов и передаем функцию преобразования, которая умножает исходный результат на коэффициент преобразования 703:
@Test void test() { var bmiCategory = bmiCategory(79.0, 167.0, bmi -> bmi * 703); // assert ... }
Заключение
В этой короткой статье мы обсудили различные способы передачи функций в функциональном программировании. Мы начали с передачи функций и лямбда-выражений в качестве входных данных для других функций: например, фильтров и картографов.
Наконец, мы изучили концепцию функций высшего порядка, которые включают в себя функции, возвращающие другие функции, такие как универсальные построители или фабричные функции. Мы также обсудили, как можно передавать функции в качестве параметров вместе с обычными входными значениями, чтобы изменить поведение алгоритма. Этот подход обычно используется в программировании для добавления обратных вызовов или настройки потока выполнения функции.
Спасибо!
Спасибо за прочтение статьи и, пожалуйста, дайте мне знать, что вы думаете! Любая обратная связь приветствуется.
Если вы хотите узнать больше о чистом коде, дизайне, модульном тестировании, объектно-ориентированном программировании, функциональном программировании и многом другом, обязательно ознакомьтесь с другими моими статьями. Вам нравится контент? Подумайте о том, чтобы подписаться или подписаться на список адресов электронной почты.
Наконец, если вы хотите стать участником Medium и поддержать мой блог, вот мой реферал.
Удачного кодирования!
Повышение уровня кодирования
Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:
- 👏 Хлопайте за историю и подписывайтесь на автора 👉
- 📰 Смотрите больше контента в публикации Level Up Coding
- 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
- 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"
🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу