Функциональное программирование в Java: определение, паттерны и применение
Как функциональное программирования в Java выглядит на деле. Для начала давайте создадим вот такой интерфейс:
public finish interface MyFunction
R apply(P form);
>
Потом возьмем и объявим анонимную реализацию выше описанного интерфейса. Например так:
public static void main()
// Объявим «анонимную функцию», присвоив ее значение переменной intAndString.
MyFunction intAndString = new MyFunction()
@Override public MyString apply(Integer from)
return from.AndString();
>
>;
intAndString.apply(8000); // Происходит вызов нашей «анонимной функции», где в ответ получим строку «8000».
>
Чуть выше мы показали, как реализовывается «анонимная функция». Если рассматривать ее с позиции функционального программирования, тогда она ничем не отличается от обычной функции функциональных языков программирования. То ест ь с ней можно делать все, что обычно делают программисты с функциями. Таким образом Java «превращается» из объектно-ориентированного языка в функциональный язык программирования.
Функциональное программировани е в Java: принципы
- языки, которые специально спроектированы для реализации функциональной парадигмы, например : Haskell, Erlang, F# и др. ;
- языки, которые поддерживают возможности объектно-ориентированного и функционального программирования, например : Java, JavaScript, Python, PHP, C++ и др. ;
- языки, которые не поддерживают реализацию функционального программирования.
- Переменные и функции. Это важнейшие составляющие функциональной парадигмы. С переменными в Java все в порядке, а вот с функциями нужно повозиться и реализовывать их через «анонимные» интерфейсы и методы.
- Чистые функции. Такие функции не создают проблемных эффектов и при идентичном входящем значении всегда выдают одинаковый вывод. Благодаря «чистым» функциям снижается риск возникновения ошибок в программе.
- Неизменяемые данные и состояния. После их определения данны е или состояни я н е могут видоизменяться. Благодаря этому свойству сохраняется постоянство рабочей среды для выводящих значений функции. При соблюдении этого принципа каждая функция воспроизводит один и тот же результат и не имеет зависимости от состояния программного обеспечения. Также такой принцип исключает применение функций с общим состоянием. Это когда в одно состояние программы упира е тся несколько функций.
- Рекурсия. Это способ осуществлять перебор информации в функциональном программировании без использования цикла «if. else».
- Первоклассность функций. Этот принцип позволяет применять функци ю к ак обычное значение. Например, можно заполнить функциями массив или сохранить их в переменной.
- Высший порядок функции. Этот принцип позволяет одной функции использовать другую функцию в качестве своего аргумента или возвращать ее в качестве исходящего значения.
- Композиция функций. Этот принцип подразумевает построение структуры из функций, где результат выполнения одной функции будет передаваться в другую функцию , и так дальше по цепочке. Таким образом, при помощи вызова одной функции можно спровоцировать исполнение целой цепочки функций.
Преимущество функционального программирования в Java
- более легкая отладка за счет использования «чистых» функций и неизменных данных;
- отложенное вычисление происходит за счет того, что функциональная программа вычисляется только при надобности;
- модульность достигается за счет того, что «чистые» функции можно использовать в разных областях одного кода;
- улучшенная читабельность достигается за счет того, что поведение каждой отдельной функции предсказуемо и неизменно;
- облегченное параллельное программирование;
- и др.
Заключение
Функциональное программирование в Java не является основной парадигмой, однако тоже довольно просто реализуется. Обычно функциональное программирование имеет смысл применять тогда, когда программные решения легко выражаются при помощи функций и не имеют тесной связи с реальным миром. ООП чаще всего реализуется, когда п рограмма моделируется с использованием объектов из реальной жизни. Подробнее на разнице между ФП и ООП мы остановимся в следующих статьях.
Мы будем очень благодарны
если под понравившемся материалом Вы нажмёте одну из кнопок социальных сетей и поделитесь с друзьями.
Функциональное программирование в Java
Сейчас появляются новые модные языки использующие парадигму функционального программирования. Тем не менее, в обычной Java
можно использовать функции для описания поведения объектов. Причём делать это можно полностью в рамках синтаксиса Java.
Я опубликовал Java-библиотеку позволяющую связывать (binding) объекты через функции (см. https://code.google.com/p/tee-binding/ )
Описание классов
public class It‹E›
— основной класс, содержит ссылку на объект любого типа и обновляет все связи при изменении значений в одном из экземпляров. Пример
It‹String› a1 = new It‹String›().value("A"); It‹String› a2 = new It‹String›().value("B"); System.out.println("a1: "+a1.value()+", a2: "+a2.value()); a1.bind(a2); System.out.println("a1: "+a1.value()+", a2: "+a2.value()); a1.value("C"); System.out.println("a1: "+a1.value()+", a2: "+a2.value()); a2.value("D"); System.out.println("a1: "+a1.value()+", a2: "+a2.value());
a1: A, a2: B a1: B, a2: B a1: C, a2: C a1: D, a2: D
Класссы Number, Note, Toggle являются производными класса It для хранения значений конкретного типа (соответственно для Double, String и Boolean) и содержат методы задания связывания с использованием функций. Пример:
Numeric c = new Numeric().value(0); Numeric f = c.multiply(9.0).divide(5.0).plus(32.0); System.out.println("f: " + f.value() + ", c: " + c.value()); System.out.println("/let f = 100 "); f.value(100); System.out.println("f: " + f.value() + ", c: " + c.value()); System.out.println("/let c = 100 "); c.value(100); System.out.println("f: " + f.value() + ", c: " + c.value());
f: 32.0, c: 0.0 /let f = 100 f: 100.0, c: 37.77777777777778 /let c = 100 f: 212.0, c: 100.0
как видно, это функция конвертации температуры из шкалы Цельсия в шкалу Фаренгейта (F’ = C’ * 9 / 5 + 32). Из определения переменной
Numeric f = c.multiply(9.0).divide(5.0).plus(32.0);
это вполне очевидно. Также можно отметить что связывание через функцию является двунаправленным.
Примечание: псевдооператоры функции вычисляются последовательно без учёта приориетета операций.
Для более сложных случаев можно использовать класс Fork. Он позволяет использовать в связывании условия, пример:
System.out.println("/n = -10"); Numeric n = new Numeric().value(-10); Note r = new Note().bind(new Fork‹String›() .condition(new Toggle().less(n, -5)) .then("Frost") .otherwise(new Fork‹String›() .condition(new Toggle().less(n, +15)) .then("Cold") .otherwise(new Fork‹String›() .condition(new Toggle().less(n, +30)) .then("Warm") .otherwise("Hot") ))); System.out.println(r.value()); System.out.println("/let n = +10"); n.value(10); System.out.println(r.value()); System.out.println("/let n = +20"); n.value(20); System.out.println(r.value()); System.out.println("/let n = +40"); n.value(40); System.out.println(r.value());
/n = -10 Frost /let n = +10 Cold /let n = +20 Warm /let n = +40 Hot
Запись условия вполне наглядна, в зависимости от значения переменной n, в переменную r заносится текст Frost, Cold, Warm или Hot.
Применение библиотеки
К сожалению, связывание и функции нельзя использовать непосредственно. Рассмотрим модификации необходимые для применения связывания в Swing:
class BindableLabel extends JLabel < private Note bindableValue = new Note().value("").afterChange(new Task() < @Override public void job() < if (bindableValue != null) < setText(bindableValue.value()); >> >); public Note bindableValue() < return bindableValue; >public BindableLabel() < super(); >>
это класс расширяющий стандартный JLabel. Он позволяет обновлять связывать текст надписи с переменно имеющий тип Note.
Для редактируемых Swing-компонентов также придётся добавить ChangeListener. Пример определения поведения компонентов из формы на скриншоте:
void bindComponents() < Numeric celsius = new Numeric().value(0); Numeric fahrenheit = celsius.multiply(9.0).divide(5.0).plus(32.0); fahrenheitSlider.bindableValue().bind(fahrenheit); fahrenheitSpinner.bindableValue().bind(fahrenheit); celsiusSlider.bindableValue().bind(celsius); celsiusSpinner.bindableValue().bind(celsius);
— как видно, это занимает всего несколько строк и при редактировании любого значения в форме (или перемещения ползунка слайдера) остальные компоненты мгновенно обновляют своё состояние.
Что такое функциональное программирование
В программировании есть два больших подхода — императивное и функциональное. Они существенно отличаются логикой работы, ещё и создают путаницу в названиях. Сейчас объясним.
Функциональное — это про функции?
❌ Нет. Функциональное — это не про функции. Функции есть почти в любых языках программирования: и в функциональных, и в императивных. Отличие функционального программирования от императивного — в общем подходе.
Метафора: инструкция или книга правил
Представьте, что вы открываете кафе-столовую. Сейчас у вас там два типа сотрудников: повара и администраторы.
Для поваров вы пишете чёткие пошаговые инструкции для каждого блюда. Например:
- Налить воды в кастрюлю
- Поставить кастрюлю с водой на огонь
- Добавить в кастрюлю с водой столько-то соли
- Если нужно приготовить 10 порций, взять одну свёклу. Если нужно приготовить 20 порций, взять две свёклы.
- Почистить всю свёклу, которую вы взяли
- …
Повар должен следовать этим инструкциям ровно в той последовательности, в которой вы их написали. Нельзя сначала почистить свёклу, а потом взять её. Нельзя посолить кастрюлю, в которой нет воды. Порядок действий важен и определяется вами. Это пример императивного программирования. Вы повелеваете исполнителем. Можно сказать, что исполнители выполняют ваши задания.
Для администратора вы пишете не инструкцию, а как бы книгу правил:
- У нас нельзя со своим. Если гости пришли со своим, то сделать им замечание такое-то.
- В зале должно быть чисто. Если в зале грязно, вызвать уборщика.
- Если образовалась очередь, открыть дополнительную кассу.
Это тоже команды, но исполнять их администратор будет не в этой последовательности, а в любой на своё усмотрение. Можно сказать, что задача этого человека — исполнять функции администратора, и мы описали правила, по которым эти функции исполнять. Это пример функционального программирования.
❌ Программисты, не бомбите
Конечно же, это упрощено для понимания. Вы сами попробуйте это нормально объяснить (можно прямо в комментах).
Императивное программирование
Примеры языков: C, С++, Go, Pascal, Java, Python, Ruby
Императивное программирование устроено так:
В языке есть команды, которые этот язык может выполнять. Эти команды можно собрать в подпрограммы, чтобы автоматизировать некоторые однотипные вычисления. В каком порядке записаны команды внутри подпрограммы, в том же порядке они и будут выполняться.
Есть переменные, которые могут хранить данные и изменяться во время работы программы. Переменная — это ячейка для данных. Мы можем создать переменную нужного нам типа, положить туда какое-то значение, а потом поменять его на другое.
Если подпрограмме на вход подать какое-то значение, то результат будет зависеть не только от исходных данных, но и от других переменных. Например, у нас есть функция, которая возвращает размер скидки при покупке в онлайн-магазине. Мы добавляем в корзину товар стоимостью 1000 ₽, а функция должна нам вернуть размер получившейся скидки. Но если скидка зависит от дня недели, то функция сначала проверит, какой сегодня день, потом посмотрит по таблице, какая сегодня скидка.
Получается, что в разные дни функция получает на вход 1000 ₽, но возвращает разные значения — так работает императивное программирование, когда всё зависит от других переменных.
Последовательность выполнения подпрограмм регулируется программистом. Он задаёт нужные условия, по которым движется программа. Вся логика полностью продумывается программистом — как он скажет, так и будет. Это значит, что разработчик может точно предсказать, в какой момент какой кусок кода выполнится — код получается предсказуемым, с понятной логикой работы.
Если у нас код, который считает скидку, должен вызываться только при финальном оформлении заказа, то он выполнится именно в этот момент. Он не посчитает скидку заранее и не пропустит момент оформления.
Суть императивного программирования в том, что программист описывает чёткие шаги, которые должны привести код к нужной цели.
Звучит логично, и большинство программистов привыкли именно к такому поведению кода. Но функциональное программирование работает совершенно иначе.
Функциональное программирование
Примеры языков: Haskell, Lisp, Erlang, Clojure, F#
Смысл функционального программирования в том, что мы задаём не последовательность нужных нам команд, а описываем взаимодействие между ними и подпрограммами. Это похоже на то, как работают объекты в объектно-ориентированном программировании, только здесь это реализуется на уровне всей программы.
Например, в ООП нужно задать объекты и правила их взаимодействия между собой, но также можно и написать просто код, который не привязан к объектам. Он как бы стоит в стороне и влияет на работу программы в целом — отправляет одни объекты взаимодействовать с другими, обрабатывает какие-то результаты и так далее.
Функциональное программирование здесь идёт ещё дальше. В нём весь код — это правила работы с данными. Вы просто задаёте нужные правила, а код сам разбирается, как их применять.
Если мы сравним принципы функционального подхода с императивным, то единственное, что совпадёт, — и там, и там есть команды, которые язык может выполнять. Всё остальное — разное.
Команды можно собирать в подпрограммы, но их последовательность не имеет значения. Нет разницы, в каком порядке вы напишете подпрограммы — это же просто правила, а правила применяются тогда, когда нужно, а не когда про них сказали.
Переменных нет. Вернее, они есть, но не в том виде, к которому мы привыкли. В функциональном языке мы можем объявить переменную только один раз, и после этого значение переменной измениться не может. Это как константы — записали и всё, теперь можно только прочитать. Сами же промежуточные результаты хранятся в функциях — обратившись к нужной, вы всегда получите искомый результат.
Функции всегда возвращают одно и то же значение, если на вход поступают одни и те же данные. Если в прошлом примере мы отдавали в функцию сумму в 1000 ₽, а на выходе получали скидку в зависимости от дня недели, то в функциональном программировании если функция получит в качестве параметра 1000 ₽, то она всегда вернёт одну и ту же скидку независимо от других переменных.
Можно провести аналогию с математикой и синусами: синус 90 градусов всегда равен единице, в какой бы момент мы его ни посчитали или какие бы углы у нас ещё ни были в задаче. То же самое и здесь — всё предсказуемо и зависит только от входных параметров.
Последовательность выполнения подпрограмм определяет сам код и компилятор, а не программист. Каждая команда — это какое-то правило, поэтому нет разницы, когда мы запишем это правило, в начале или в конце кода. Главное, чтобы у нас это правило было, а компилятор сам разберётся, в какой момент его применять.
В русском языке всё работает точно так же: есть правила правописания и грамматики. Нам неважно, в каком порядке мы их изучили, главное — чтобы мы их вовремя применяли при написании текста или в устной речи. Например, мы можем сначала пройти правило «жи-ши», а потом правило про «не с глаголами», но применять мы их будем в том порядке, какой требуется в тексте.
Получается, что смысл функционального программирования в том, чтобы описать не сами чёткие шаги к цели, а правила, по которым компилятор сам должен дойти до нужного результата.
Курсы по программированию с нуля
Приходите к нам в ИТ. У нас есть удаленная работа, высокие зарплаты и удобное обучение в «Яндекс Практикуме». Старт бесплатно.
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Функциональное программирование в Java
Что такое функциональное программирование? Если в двух словах, то функциональное программирование — это программирование, в котором функции являются объектами, и их можно присваивать переменным, передавать в качестве аргументов другим функциям, возвращать в качестве результата от функций и т. п. Преимущества, которые раскрывает такая возможность, будут понятны чуть позже. Пока нам надо разобраться, как в Java можно использовать саму конструкцию «функция».
Как известно, в Java нету функций, там есть только классы, методы и объекты классов. Зато в Java есть анонимные классы, то есть классы без имени, которые можно объявлять прямо в коде любого метода. Этим мы и воспользуемся. Для начала объявим такой интерфейс:
public final interface Function
Теперь в коде какого-нибудь метода мы можем объявить анонимную реализацию этого интерфейса:
public static void main() < // Объявляем "функцию", присваиваем ее переменной intToString. FunctionintToString = new Function() < @Override public String apply(Integer from) < return from.toString(); >>; intToString.apply(9000); // Вызываем нашу функцию. Получаем строку "9000". >
Такую реализацию мы и будем называть «анонимной функцией». С точки зрения функционального программирования с ней можно делать все то же самое, что и с функцией из функциональных языков: присваивать переменным, передавать в качестве аргумента другим функциям(и методам классов), получать в качестве результата от функций(и методов классов).
Теперь можно перейти к изложению некоторых базовых паттернов функционального программирования.
Работа с коллекциями в функциональном стиле
Допустим, у нас есть некая коллекция целых чисел. Мы хотим их вывести на экран в виде строки, и каждое число в строке будет разделено через запятую. Нефункциональное решение выглядело бы примерно так:
public String joinNumbers(Collection numbers) < StringBuilder result = new StringBuilder(); boolean first = true; for (Integer number : numbers) < if (first) first = false; else result.append(", "); result.append(number); >return result; >
Для реализации функционального решения нам потребуется сперва подготовить несколько функций и методов. Будем объявлять их в качестве статических полей класса:
public static final Function INT_TO_STRING = . // Уже реализовали выше // Берет поэлементно значения из коллекции from, преобразует их с помощью функции transformer // и возвращает список результатов преобразования в том же порядке. public static List map(Collection from, Function transformer) < ArrayListresult = new ArrayList(); for (F element : from) result.add(transformer.apply(element)); return result; > // Берет коллекцию произвольных элементов и конкатенирует их в строку public static String join(Collection from, String separator) < StringBuilder result = new StringBuilder(); boolean first = true; for (T element : from) < if (first) first = false; else result.append(separator); result.append(element); >return result.toString(); >
Теперь наш метод joinNumbers будет выглядить следующим образом:
public String joinNumbers(Collection numbers)
Метод реализован ровно в одну простую строку.
- Методы map и join являются достаточно обобщенными, то есть их можно применять не только для решения данной задачи. Это значит, что их можно было бы выделить в некий утилитный класс, и использовать потом этот класс в разных частях проекта.
- Вместо класса Collection в методе map можно было бы передавать Iterable и возвращать новый Iterable , извлекая из переданной коллекции данные по мере обхода данных в возвращаемой коллекции, то есть извлекать элементы лениво, поэтапно, а не все сразу. Такая реализация, позволит, например, создавать цепочки преобразования данных, выделяя каждый этап преобразования в отдельную простую функцию, при этом эффективность алгоритма будет оставаться порядка O(n):
map(map(numbers, MULTIPLY_X_2), INT_TO_STRING); // каждый элемент умножаем на два и приводим к строке. - Создавая какой-нибудь класс, вы можете создавать для некоторых его методов статические поля, являющиеся функциями-обертками, делегирующими вызов apply на вызов соответствующего метода класса. Это позволит использовать «методы» объектов в функциональном стиле, например, в представленных выше конструкциях.
Работа с коллекциями с помощью Google Collections
- interface Function . Интерфейс, аналогичный приведенному мной выше.
- Iterables.filter . Берет коллекцию и функцию-предикат(функцию, возвращающую булево значение). В ответ возвращает коллекцию, содержающую все элементы исходной, на которые указанная функция вернула true. Удобно, например, если мы хотим отсеить из коллекции все четные числа: Iterables.filter(numbers, IS_ODD);
- Iterables.transform . Делает то же самое, что функция map в моем примере выше.
- Functions.compose . Берет две функции. Возвращает новую функция — их композицию, то есть функцию, которая получает элемент, подает его во вторую функцию, результат подает в первую функцию, и полученный из первой функции результат возвращает пользователю. Композицию можно использовать, например, так: Iterables.transform(numbers, Functions.compose(INT_TO_STRING, MULTIPLY_X_2));
В Google Collections конечно есть еще много других полезных вещей как для функционального программирования, так и для работы с коллекциями в императивном стиле.
Ссылки
- Статья в Википедии о функциональном программировании.
- Google Guava, проект, частью которого является Google Collections.
- Видеопрезентация Google Collections с Joshua Bloch.
- Apache Commons Collections. Решает схожие с Google Collections задачи, но был написан под Java 4, то есть без параметрических типов.
О чем хотелось бы рассказать еще
- Мутабельные и иммутабельные замыкания.
- Pattern-matcher.
- Монады.
- Распараллеливание с использованием функционального подхода.
- Комбинаторы парсеров.