diff --git a/lessons/java-core/048/Functional programming in Java.md b/lessons/java-core/048/Functional programming in Java.md new file mode 100644 index 0000000..4513853 --- /dev/null +++ b/lessons/java-core/048/Functional programming in Java.md @@ -0,0 +1,365 @@ +# Функциональное программирование в Java + +Как уже говорилось ранее, в текущем разделе мы попробуем охватить возможности функционального программирования, которые +предлагает Java. + +В рамках данного урока постараемся разобраться с концептуальной разницей **императивной** и **функциональной парадигм +программирования**, понять отношения между ООП и ФП, а также постараемся на примере нескольких блоков кода разобраться, +в чем ключевая разница между императивным и функциональным подходом в Java. + +## Теория. Скучно, нудно, но нужно + +Строго говоря, текущую статью нельзя назвать статьей о функциональном программировании. Теоретическую часть этой +концепции мы затронем совсем поверхностно, ровно настолько, насколько необходимо для понимания основных механизмов ФП в +Java. Возможно, мы еще вернемся к базису ФП в конце курса, но в целом – я не вижу реальной необходимости в данных +знаниях для junior-специалистов. Все же, глубокое понимание ООП для Java-разработчика намного важнее. + +Итак, что такое императивная и функциональная парадигмы и в чем их различие? + +Для начала, постараемся понять, что такое **парадигма программирования**. + +Парадигма программирования – набор основополагающих концепций и подходов, определяющих стиль написания программного +кода. Какие-то парадигмы могут использоваться совместно, какие-то являются взаимоисключающими. По крайней мере, +теоретически. + +Ниже приведем примеры подходов, характерные для разных парадигм. + +С объектно-ориентированной парадигмой мы уже знакомы. Она основана на представлении информационной системы (и, как +следствие, кода, который эту систему описывает) в виде взаимодействия различных объектов, где каждый объект – экземпляр +определенного шаблона (класса). Понятия объектов, классов, а также правила их организации на различных уровнях +взаимодействия и представляют собой концепции и подходы, характерные для данной парадигмы. + +**Императивная парадигма** – парадигма, основанная на написании программного кода через последовательно выполняющиеся +инструкции, характеризующаяся стремлением к сохранению состояния (скажем, в переменных). + +Строго говоря, код, который мы писали в рамках курса, является примером императивного программирования. + +Раскрывая более подробно, у императивного подхода есть следующие отличительные черты: + +- Использование переменных. Необходимо для сохранения состояния программы; + +> **Состояние** – множество параметров, описывающих программу (в узком смысле – подпрограмму, метод) в конкретный момент +> времени. Сама система (программа) в таком случае представляет собой набор сменяющих друг друга состояний. + +- Использование оператора присваивания. + +> В узком смысле – именно этот оператор позволяет состояние записывать: +> +> ``` +> int i = 5; +> ``` +> +> Переменная `i` находится в состоянии (имеет значение) пять. +> В более широком смысле, этот оператор позволяет и изменять состояние, базируясь на предыдущем значении: +> +> ``` +> i = i + 6; +> ``` +> +> Состояние (значение) `i` представляется как состояние до текущей операции `+ 6`. К слову, подобные конструкции многих +> отпугивают от программирования на ранних этапах; + +- Использование подпрограмм. + +> Подпрограммой будем считать обособленный именованный набор инструкций: функций (в императивном понимании), процедур. +> В случае Java – методов. + +Полагаю, каждый из пунктов вам знаком на практике в той, или иной степени. Вероятно, большинство не понимают, как вообще +возможен какой-то другой подход. Это нормально, ведь Java – императивный язык. Остальные парадигмы, которые в нем +реализованы (в том числе, ООП), вынуждены с этим считаться. + +**Функциональная парадигма** – как один из представителей **декларативной парадигмы**, может быть описана как подход, +основанный не на наборе инструкций «как сделать», а на общей концепции – **спецификации** – объясняющей, что ожидается +на выходе из программы (подпрограммы). + +Определение выше популярное, очень широкое и мало что объясняющее. Поэтому попробуем проще. + +Функциональный подход предполагает написание программ, как набора функций в математическом плане: `f(x) = 2x` и вот это +вот все. + +В таком подходе, теоретически, нет необходимости в сохранении состояния в переменных – достаточно входных параметров, а +далее эти параметры будут обрабатываться функциями (как `х` для получения `у`), а результат каждой функции будет +обрабатываться следующей функцией до тех пор, пока не будет получен конечный результат, который и будет, так или иначе, +отправлен пользователю. + +На практике, безусловно, необходимы и переменные, и сам переход функционально-описанного кода в императивный – чтобы +процессор смог понять, что же от него требуется. + +Но функциональная парадигма является более высокоуровневой, чем императивная. Т.е. имеет больший уровень абстракции. +Поэтому состояния, присваивания и последовательные инструкции в той или иной степени скрыты от программиста. В каких-то +языках (в т.ч. в Java) – в меньшей степени, в каких-то – в большей. + +Рассматривая особенности функциональной парадигмы, можно выделить следующие характеристики (не все из них действительно +актуальны для функционального стиля в Java): + +- Функция, вызванная с одними и теми же параметрами, всегда вернет одинаковый результат. В императивном подходе + результат может разниться, если учитывает какой-то "внешний" контекст; +- Код представляет собой набор цепочек из обращений к функциям. Визуально может отличаться из-за особенностей синтаксиса + конкретного языка, но смысл обычно схожий; +- Используемые типы – _immutable_. Это могут быть примитивы, объекты (или другие структуры), используемые в качестве + аргументов или результатов вычисления функций. Локальные переменные – отсутствуют; +- За счет отсутствия переменных как способа хранения состояния – отсутствуют циклы в привычном виде. Зачастую цикл + заменяется рекурсией. + +> Грубо говоря, итеративный (с помощью цикла) способ вычисления факториала, невозможен без сохранения промежуточного +> значения в переменных. А рекурсивный – возможен. + +Очень важно понимать, что практически все популярные на данный момент языки программирования – императивные. И поэтому +реализации функционального программирования в них так или иначе вынуждены опираться на тот императивный базис, который +существует. Поэтому, несмотря на красивую теорию об отсутствии состояния, во многих задачах это состояние приходится +хранить. Отчасти из-за ограничений языка, отчасти из соображений оптимизации. + +Полагаю, на данном этапе уже понятно, что теоретически функциональный (глядя шире - декларативный) подход можно +противопоставлять императивному. И это, в сущности, верно, ведь эти подходы исходят из изначально разных концепций. + +В контексте сравнения объектно-ориентированного и функционального подхода, все немного интереснее. + +ООП, так или иначе, детище императивной парадигмы. Но де-факто, ООП определяет, в первую очередь, верхнеуровневое +взаимодействие компонентов системы (классов). Наследование, инкапсуляция, даже полиморфизм, направлены на построение +архитектуры решения. В сущности, ни один из этих принципов не регламентирует способы обработки данных внутри методов. + +Таким образом, объектно-ориентированная парадигма не противоречит и не конфликтует с функциональной – они проявляют себя +на разных уровнях организации кода, если утрировать. + +Безусловно, при реализации проекта с использованием функциональной парадигмы, внешний вид системы будет отличаться от +вида, получившегося при использовании императивной. Но заметить это можно лишь открыв конкретные классы, посмотрев на +методы (иногда – уже на уровне декларации, не говоря о теле). Но никогда – на уровне диаграммы классов. По крайней мере, +в Java. + +## Примеры императивной и функциональной реализаций в Java + +На данном этапе, не факт, что примеры в функциональном стиле будут полностью понятны – инструменты, которые в них +используются, мы будем рассматривать в ближайших уроках. Но общий смысл будет понятен уже сейчас, как и контраст с +императивным стилем. + +Пример 1: + +Существует переменная, ссылающаяся на объект типа _Квартира_ – `«Flat»`. Допустим, что класс `Flat` имеет поле `family` +соответствующего типа `Family`, указывающий на семью, которая в этой квартире живет. В классе `Family` есть +поле `father` типа `Human` – отец семейста. У `Human` есть свое поле `father` того же типа `Human` – отец конкретного +человека. Также класс `Human` содержит поле `car` типа `Car` – машина, принадлежащая человеку. И тип `Car` содержит поле +_«номер»_, обозначающий номер машины. + +Опустим для лаконичности модификаторы доступа, не интересующие нас поля, конструкторы, геттеры и сеттеры и опишем +классы, обозначенные выше: + +```java + class Flat { + Family family; + } + + class Family { + Human father; + } + + class Human { + Human father; + Car car; + } + + class Car { + String number; + } +``` + +Итак, перед нами стоит задача написать метод, возвращающий номер машины, принадлежащей отцу человека, который является +отцом в семье, проживающей в конкретной квартире. В теории все просто: + +```java + String getCarNumberBySonFlat(Flat flat) { + return flat.getFamily() + .getFather() + .getFather() + .getCar() + .getNumber(); + } +``` + +Но оказалось, что сама квартира может еще не существовать, да и семьи в ней может не жить, или отец семейства как вышел +лет 20 назад за сигаретами, так и пропал… + +В общем, каждое из значений может оказаться `null`. В таком случае, реализация выше имеет все шансы упасть с +`NullPointerException`. С этого момента наша императивная жизнь превращается в локальный ад. У ада могут быть разные +уровни вложенности _if_-ов, в зависимости от выбранной реализации, но не суть. +Опишем более-менее читабельную реализацию: + +```java + String getCarNumberBySonFlat(Flat flat) { + if (flat == null) { + return null; + } + + Family family = flat.getFamily(); + if (family == null) { + return null; + } + + Human familyFather = family.getFather(); + if (familyFather == null) { + return null; + } + + Human father = familyFather.getFather(); + if (father == null) { + return null; + } + + Car car = father.getCar(); + if (car == null) { + return null; + } + + return car.getNumber(); + } +``` + +> !NB: на некоторых языках, в том числе JS, данный код можно было бы привести практически к первой реализации, будь +> проблема лишь в проверке на `null`. Но пример условный, а Java – далеко не самый лаконичный язык. + +Итак, как обстоят дела в императивной парадигме примерно понятно. Такой код мог бы написать каждый из вас уже давно. Но +что предлагает Java для функциональной реализации той же задачи? + +```java + String getCarNumberBySonFlat(Flat flat) { + return Optional.ofNullable(flat) + .map(Flat::getFamily) + .map(Family::getFather) + .map(Human::getFather) + .map(Human::getCar) + .map(Car::getNumber) + .orElse(null); + } +``` + +Полагаю, не вызывает сомнений, что такой код более лаконичный. При ситуации, когда результатом какой-то из промежуточных +функций окажется `null` – остаток цепочки будет пропущен и вернется значение по умолчанию, указанное в `orElse()`. В +нашем случае – тоже `null`, но можно было указать и другое. + +Код в примере выглядит достаточно пресно. В конце концов, можно было обернуть все в _try-catch_ и просто отловить +возможную _NPE_. Некрасиво, но тоже работает. + +Однако достаточно заменить каждую из проверок на `null` на валидацию по другим условиям (только для трехкомнатной +квартиры, только если отец семейства не курит…) и простые решения исчезнут, останется лишь закапываться в _if_’ы. +Функциональная реализация в таком случае тоже претерпит некоторые изменения, но останется не менее лаконичной. В рамках +примера оставим проверки на `null`, чтобы не вводить лишние поля, раздувая исходные классы. + +Пример 2 + +Существует компания (`Company`), внутри которой существует список отделов (`Department`), в каждом из которых есть +список сотрудников (`Employee`). Необходимо получить мапу, в которой ключом будет возраст сотрудника, а значением – +список сотрудников подходящего возраста. Вишенка на торте – список сотрудников в каждом из отделов может содержать +_null_’ы. + +Вкратце опишем классы, необходимые для данного примера: + +```java + class Company { + List departments; + } + + class Department { + List employees; + } + + class Employee { + int age; + } +``` + +Решение задачи в императивном стиле может быть чуть лучше или чуть хуже, в зависимости от того, насколько вы владеете +коллекциями и умеете в алгоритмизацию, но будет примерно таким: + +```java + Map> getEmployeesByAge(Company company) { + List employees = new ArrayList<>(); + for (Department department : company.getDepartments()) { + for (Employee employee : department.getEmployees()) { + if (employee != null) { + employees.add(employee); + } + } + } + + Map> employeesByAge = new HashMap<>(); + for (Employee employee : employees) { + int age = employee.getAge(); + if (!employeesByAge.containsKey(age)) { + employeesByAge.put(age, new ArrayList<>()); + } + employeesByAge.get(age).add(employee); + } + + return employeesByAge; + } +``` + +Описывая алгоритм, нам необходимо сформировать список всех сотрудников из разных отделов, игнорируя _null_’ы, а потом +сформировать мапу и наполнить ее сотрудниками. Для каждой итерации добавления сотрудника в мапу будем проверять – +существует ли список под конкретный возраст, если нет – создаем список и лишь потом добавляем сотрудника. + +Теперь решение той же задачи в функциональном стиле: + +```java + Map> getEmployeesByAge(Company company) { + return company.getDepartments() + .stream() + .map(Department::getEmployees) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(Employee::getAge)); + } +``` + +В данном случае алгоритм выглядит примерно так: + +1. Отделы компании представим в виде _Stream_'а (еще выясним, что за он); +2. Представим каждый отдел как список сотрудников; +3. Преобразуем стрим, сделав его единицей не список сотрудников - а одиночного сотрудника; +4. Оставим лишь тех сотрудников, которые не `null`; +5. Соберем данные в коллекцию посредством группировки, указав, что группировать единицы данных (сотрудников) будем по + возрасту. + +Полагаю, вы уже догадались, что лямбда-выражения (или, как в данном случае, _method reference_’ы) концептуально +представляют собой те самые аналоги математических функций, на которых и строится функциональное программирование – +обрабатывая результат одной функции следующей функцией, мы не имеем явной необходимости в лишних переменных, фиксирующих +промежуточные состояния. Для многих классических задач методы могут быть сведены к вызову `return` и цепочки вызовов над +входным параметром (или параметрами), которые в итоге приведут к ожидаемому результату. + +При этом методы, в которые передаются лямбды - указывают, какого типа преобразование мы хотим увидеть - из одного типа +данных в другой, отфильтровать по какому-то признаку и пр. А сами лямбда-выражения описывают механизм преобразования - +что именно хотим получить на выходе. + +Условно, строчку + +``` + .map(Department::getEmployees) +``` + +Можно описать как "представить каждый отдел как список его сотрудников". + +## Подводя итог + +Функциональный подход в Java – очень мощный инструмент в обработке данных. Это в меньшей степени очевидно на учебных +примерах, но весьма чувствительно на реальных проектах, когда многие задачи сводятся к получению одних массивов данных +из других, фильтрации и агрегации (группировке) полученных данных. В общем-то, оба примера выше – вольная интерпретация +реальных задач из практики автора. Разве что немного упрощенная. + +В следующих уроках мы разберемся и с тем, что такое `Optional`, и с тем, что такое `Stream` (нет, к I/O Stream’ам это +отношения не имеет). + +Пока же советую хотя бы базово уложить в голове, что такое парадигма программирования и как соотносятся императивный, +функциональный и объектно-ориентированные подходы между собой. Не ограничивайтесь данной статьей. Ее задача – лишь +заинтересовать функциональным подходом в Java и дать хоть какой-то плацдарм, на котором можно строить восприятие +описанных концепций. + +#### С теорией на сегодня все! Урок теоретический, практику отложим до следующего урока. + +![img.png](../../../commonmedia/justTheoryFooter.png) + +> Если что-то непонятно или не получается – welcome в комменты к посту или в лс:) +> +> Канал: https://t.me/ViamSupervadetVadens +> +> Мой тг: https://t.me/ironicMotherfucker +> +> **Дорогу осилит идущий!** \ No newline at end of file