Java.next: Стили написания функционального кода

Функциональные конструкты, общие для языков Groovy, Scala и Clojure, и их преимущества

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

Нил Форд, Архитектор приложений, ThoughtWorks

Нил Форд (Neal Ford) работает архитектором приложений в ThoughtWorks, революционной компании, предоставляющей профессиональные IT-услуги и помогающей талантливым людям по всему миру эффективнее использовать программное обеспечение. Он также является проектировщиком и разработчиком приложений, учебных материалов, журнальных статей, учебных курсов, видео/DVD-презентаций и автором книг "Разработка с Delphi: Объектно-ориентированный подход", "JBuilder 3 Unleashed" и "Искусство разработки Web-приложений на Java". Он специализируется на консультациях по построению широкомасштабных корпоративных приложений. Он также является общепризнанным докладчиком и выступал на многочисленных конференциях разработчиков по всему миру. С ним можно связаться по адресу: nford@thoughtworks.com.



26.03.2014

Об этом цикле статей

Java-технология оставит в наследство не язык, а платформу. На платформе JVM работают более 200 языков программирования; какой-то из них со временем неминуемо вытеснит язык Java в качестве наилучшего способа программирования для JVM. Этот цикл статей посвящен исследованию трех языков нового поколения для платформы JVM — Groovy, Scala и Clojure — а также сравнению и противопоставлению новых возможностей и парадигм, чтобы дать Java-разработчикам возможность заглянуть в свое ближайшее будущее.

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

Языки Java.next занимают разное положение в спектре между императивными и функциональными языками, однако все они обладают функциональными возможностями и соответствующими средствами. Приемы функционального программирования четко определены, однако зачастую для обозначения одинаковых функциональных концепций в разных языках используются разные термины, что затрудняет выявление сходства между ними. В этой статье я сравниваю стили написания функционального программного кода в языках Scala, Groovy и Clojure, а затем рассматриваю их преимущества.

Императивная обработка

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

Императивная обработка
public class TheCompanyProcess {
    public String cleanNames(List<String> listOfNames) {
        StringBuilder result = new StringBuilder();
        for(int i = 0; i < listOfNames.size(); i++) {
            if (listOfNames.get(i).length() > 1) {
                result.append(capitalizeString(listOfNames.get(i))).append(",");
            }
        }
        return result.substring(0, result.length() - 1).toString();
    }

    public String capitalizeString(String s) {
        return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
    }
}

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

Императивное программирование поощряет разработчика к выполнению операций на низком уровне. В методеcleanNames() в листинге 1, я выполняю три задачи: Я фильтрую список, чтобы устранить одиночные символы, трансформирую список, преобразуя каждое имя в верхний регистр символов, а затем преобразую этот список в единственную строку. В императивных языках я вынужден использовать один и тот же низкоуровневый механизм (итеративное прохождение по списку) для всех трех задач. Функциональные языки подходят к фильтрации, трансформации и преобразованию как к типовым операциям, поэтому они позволяют программисту атаковать проблему с другого направления.


Функциональная обработка

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

listOfEmps -> filter(x.length > 1) -> transform(x.capitalize) -> 
   convert(x, y -> x + "," + y)

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

При использовании Scala

В листинге 2 пример обработки из листинга 1 реализован на языке Scala. Текст примера очень похож на предыдущий псевдокод с необходимыми деталями реализации.

Обработка на языке Scala
val employees = List("neal", "s", "stu", "j", "rich", "bob")
val result = employees
  .filter(_.length() > 1)
  .map(_.capitalize)
  .reduce(_ + "," + _)

Сначала я фильтрую имеющийся список имен, устраняя из него имена, длина которых не превышает 1. Результат этой операции поступает в функцию map(), которая исполняет переданный ей фрагмент кода для каждого элемента в коллекции и возвращает преобразованную коллекцию. И, наконец, полученная на выходе map() коллекция поступает в функцию reduce(), которая соединяет элементы в единую строку на основе правил, предоставленных в вышеупомянутом фрагменте кода. В этом случае я соединяю каждую пару элементов посредством конкатенации со вставкой запятой. Меня не волнует, какие имена параметров присутствуют в любом из этих трех вызовов функции, поэтому я могу использовать удобный подход Scala, заменяя пропущенные имена символом "_". Функция reduce() начинает свою работу с первых двух элементов — она соединяет их конкатенацией в один элемент, который становится первым элементом для следующей конкатенации. По мере того как функция reduce() "проходит" вниз по списку, она создает требуемую строку с разделяющими запятыми.

Я показал реализацию на Scala первой по причине ее достаточно знакомого синтаксиса и потому, что Scala использует привычные для ИТ-отрасли названия filter, map, reduce для операций фильтрации, отображения и преобразования, соответственно.

При использовании Groovy

Язык Groovy обладает аналогичными возможностями, однако используемые при этом имена больше напоминают скриптовые языки, такие как Ruby. В листинге 3 представлена Groovy-версия примера, показанного ранее в листинге 1.

Обработка на языке Groovy
class TheCompanyProcess {
  public static String cleanUpNames(List listOfNames) {
    listOfNames
        .findAll {it.length() > 1}
        .collect {it.capitalize()}
        .join(',')
  }
}

Groovy-пример в листинге 3 структурно подобен Scala-примеру в листинге 2, однако имена методов отличаются. В Groovy collection-метод findAll применяет переданный фрагмент кода, сохраняя те элементы, для которых этот фрагмент кода возвращает значение true. Как и в Scala, в языке Groovy имеется механизм неявной параметризации, реализованный посредством заранее заданного неявного параметра it для фрагментов кода с одним аргументом. Метод collect— аналог map в языке Groovy — исполняет предоставленный фрагмент кода с каждым элементом коллекции. В Groovy имеется функция (join()) осуществляющая конкатенацию коллекции строк в одну строку с использованием заданного разделителя. Это именно то, что мне нужно для этого примера.

При использовании Clojure

Clojure — это функциональный язык, в котором используются имена функций reduce, map, and filter (см. листинг 4).

Обработка на языке Clojure
(defn process [list-of-emps]
  (reduce str (interpose "," 

      (map clojure.string/capitalize 
          (filter #(< 1 (count %)) list-of-emps)))))

Clojure-макрос thread-last

Макрос thread-last облегчает работу с коллекциями. Аналогичный Clojure-макрос thread-first, облегчает взаимодействие с API-интерфейсами Java. Рассмотрим весьма характерный пример Java-кода person.getInformation().
getAddress().getPostalCode()
, который наглядно демонстрирует склонность Java нарушать Закон Деметры. Этот тип утверждений вызывают определенное раздражение, поскольку заставляет Clojure-разработчиков, "потребляющих" API-интерфейсы Java, создавать вывернутые наизнанку утверждения, такие как (getPostalCode (getAddress (getInformation person))). Макрос thread-first устраняет этот раздражающий синтаксис. Этот макрос можно использовать для написания вложенных вызовов (например, -> person getInformation getAddress getPostalCode)), с необходимым количеством уровней.

Если вы не привыкли читать текст на Clojure, структура кода в листинге 4 может показаться непонятной. Диалекты языка Lisp, такие как Clojure, действуют "наоборот", поэтому местом для начала работы является заключительное значение параметра, т. е., list-of-emps. Clojure-функция (filter ) принимает два параметра: функцию (в данном случае анонимную) для использования при фильтрации и коллекцию, подлежащую фильтрации. Для первого параметра можно написать формальное определение функции, такое как (fn [x] (< 1 (count x))), однако в случае Clojure анонимные функции можно представить лаконичнее. Как и в предыдущих примерах, результатом операции фильтрации является коллекция уменьшенного размера. Функция (map ) в качестве первого параметра принимает функцию преобразования, а в качестве второго параметра — коллекцию, которая в этом случае является возвращаемым значением операции (filter ). Первым параметром Clojure-функции (map ) часто является предоставленная разработчиком функция, однако в этом качестве сможет работать любая функция, принимающая единственный параметр; встроенная функция capitalize соответствует этому требованию. И, наконец, результат операции (map ) становится параметром коллекции для функции (reduce ). Первый параметр функции (reduce )— это комбинация функции ((str ), примененной к возвращаемому результату функции (interpose ). Функция (interpose ) вставляет свой первый параметр между каждыми соседними элементами коллекции (но не после последнего элемента).

Даже опытные разработчики испытывают сложности, когда функциональные вызовы становятся слишком глубоко вложенными, как это происходит с функцией (process ) в листинге 4. К счастью, в языке Clojure имеются макросы, которые позволяют разработчику "разматывать" структуры в более удобном для чтения порядке. По своей функциональности версия в листинге 5 идентична версии в листинге 4.

Использование Clojure-макроса thread-last
(defn process2 [list-of-emps]
  (->> list-of-emps
       (filter #(< 1 (count %)))
       (map clojure.string/capitalize)
       (interpose ",")
       (reduce str)))

Clojure-макрос thread-last берет обычную операцию применения различных преобразований к коллекциям и обращает типичный порядок языка Lisp, восстанавливая более естественный порядок чтения "слева направо". В листинге 5, сначала следует коллекция (list-of-emps) Каждая последующая форма во фрагменте применяется к предыдущей. Одной из мощных сторон языка Lisp является его синтаксическая гибкость: если какой-либо код становится трудным для чтения, вы можете изменить его синтаксис для улучшения удобочитаемости.


Преимущества функционального программирования

В известном эссе под названием Beating the Averages," (Побеждая посредственность) Пол Грэм (Paul Graham) определяет т. н. " Парадокс Блаба: (Blub paradox): Он изобретает воображаемый язык под названием Блаб (Blub) и строит свои дальнейшие рассуждения на сравнении возможностей Blub с возможностями других языков.

Когда гипотетический Blub-программист анализирует менее мощный язык, он понимает, что в "континууме языковой мощи" он смотрит "сверху вниз". Он прекрасно осознает ситуацию, когда язык Blub мощнее других языков, поскольку в этих языках отсутствуют те или иные возможности, которые он использует при работе на языке Blub. Однако когда этот гипотетический Blub-программист смотрит в континууме языковой мощи в другом направлении, он не осознает, что смотрит "снизу вверх". Он просто видит странные языки. Он может считать эти языки эквивалентными по мощи языку Blub, но с непонятными ему дополнительными средствами. Язык Blub достаточно хорош для него, поскольку он "думает на языке Blub".

Многим Java-разработчикам код в листинге 2 кажется чуждым и странным, поэтому им трудно оценить его преимущества. Однако когда вы прекращаете чрезмерную детализацию выполнения задач, вы обретаете свободу использования все более интеллектуальных языков и сред исполнения, что открывает возможности для значительных усовершенствований. Например, появление виртуальной Java-машины (JVM) — которая избавила разработчика от забот по управлению памятью — стимулировало целое направление научно-исследовательской деятельности по созданию современных механизмов сборки мусора. При императивном программировании разработчик озабочен деталями того, как работают итерации цикла, что существенно затрудняет оптимизацию, например, распараллеливание. Осмысление операций на более высоком уровне (например, filter, map, reduce) отделяет концепцию от реализации, что превращает модернизацию кода, например, распараллеливание, из сложного детализированного мероприятия в простое изменение API-интерфейса.

Подумайте немного о том, как сделать код в листинге 1 многопоточным. Поскольку вы до мельчайших деталей представляете, что происходит во время цикла for, вам придется иметь дело со сложным кодом параллельного исполнения. Теперь рассмотрим распараллеленную Scala-версию, показанную в листинге 6.

Распараллеливание процесса
val parallelResult = employees
  .par
  .filter(f => f.length() > 1)
  .map(f => f.capitalize)
  .reduce(_ + "," + _)

Единственная разница между листингом 2 и листингом 6— добавление метода .par к потоку команд. Метод .par возвращает параллельную версию коллекции, с который работают последующие операции. Поскольку я специфицировал операции с коллекцией на основе концепции более высокого порядка, обеспечивающая среда исполнения имеет возможность выполнить больший объем работы.

Императивные объектно-ориентированные языки имеют тенденцию к повторному использованию на уровне классов, поскольку поощряют разработчиков к использованию классов в качестве стандартных блоки. Функциональные языки программирования имеют тенденцию к повторному использованию на уровне функций. Функциональные языки строят изощренные типовые конструкции (такие как filter(), map(), and reduce()) и предоставляют возможность их адаптации посредством функций, предоставляемых в качестве параметров. Для функциональных языков характерно преобразование структур данных в стандартные коллекции (списки и схемы), поскольку после этого ими можно манипулировать с помощью мощных встроенных функций. Например, в мире Java существуют десятки сред для обработки XML, каждая из которых ограничивается собственным представлением структуры XML и действует на основе соответствующих собственных методов. В таких языках, как Clojure, XML преобразуется в стандартную map-структуру данных, открытую для применения уже имеющихся в этом языке мощных операций преобразования, редуцирования и фильтрации.


Заключение

Все современные языки уже содержат или позволяют добавлять конструкции функционального программирования, что в значительной части определяет будущую деятельность разработчиков. Во всех языках Java.next реализованы мощные функциональные возможности (которые иногда различаются по названию и поведению). В этой статье я проиллюстрировал новый стиль написания кода на языках Scala, Groovy и Clojure, а также показал некоторые его преимущества.

В следующей статье я подробнее рассмотрю различия этих языков применительно к реализации операций filter, map и reduce.

Ресурсы

Научиться

  • Оригинал статьи: Java.next: Functional coding styles.
  • Scala: современный функциональный язык, работающий на платформе JVM.
  • Groovy: динамическая разновидность Java с обновленным синтаксисом и расширенными возможностями.
  • Clojure: современный функциональный вариант Lisp, работающий на платформе JVM.
  • Law of Demeter: (закон Деметры): правило проектирования при разработке программного обеспечения, не рекомендующее применять длинные списки для доступа к свойствам.
  • Beating the Averages (Побеждая посредственность): Пол Грэм (Paul Graham), апрель 2003 г. Пол Грэм описывает свой опыт по созданию ViaWeb – первого онлайнового сайта электронной коммерции на основе принципа "сделай сам".
  • Функциональное мышление: колонка Нила Форда на developerWorks, посвященная функциональному программированию.
  • Записная книжка дизайнера языка: : в этом цикле статей developerWorks архитектор языка Java Брайан Гетц исследует некоторые проблемы дизайна языка, вызвавшие трудности при эволюции языка Java версий Java SE 7, Java SE 8 и далее.
  • Раздел developerWorks, посвященный Java-технологии: : Сотни статей по всем аспектам программирования на Java.

Получить продукты и технологии

Обсудить

  • Присоединяйтесь к сообществу developerWorks . Связывайтесь с другими пользователями developerWorks и знакомьтесь с ориентированными на разработчиков форумами, блогами, группами и вики-ресурсами

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=966771
ArticleTitle=Java.next: Стили написания функционального кода
publish-date=03262014