Функциональное мышление: Часть 2. Функциональные возможности Groovy

Метапрограммирование и Functional Java

В языке Groovy возможности метапрограммирования эффективно соединяются с функциональным программированием. Узнайте, как метапрограммирование позволяет добавлять к типу данных Integer методы, использующие встроенные функциональные возможности Groovy. Также в этой статье рассказывается, как использовать метапрограммирование для "прозрачной" интеграции богатых функциональных возможностей инфраструктуры Functional Java в Groovy.

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

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



30.07.2012

Об этой серии статей

Эта серия статей призвана переориентировать читателя в "функциональном" направлении и помочь ему взглянуть на проблемы под новым углом, чтобы улучшить повседневное написание кода. В ней изучаются принципы функционального программирования и инфраструктуры, позволяющие использовать функциональное программирование в языке Java. Так, в статьях рассматриваются функциональные языки программирования, работающие на виртуальной Java-машине, и некоторые направления будущего развития языков программирования. Эта серия статей предназначена для программистов, знающих язык Java и работу его абстракций, но не имеющих опыта в использовании функциональных языков.

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

В языке Groovy сочетается несколько парадигм программирования. Так, он поддерживает объектно-ориентированное программирование, метапрограммирование и элементы функционального программирования, которые во многом ортогональны друг другу (см. вкладку «Ортогональность»). Метапрограммирование позволяет добавлять новую функциональность в язык и его базовые библиотеки. Сочетая метапрограммирование и функциональное программирование, можно сделать собственный код более функциональным или усовершенствовать сторонние функциональные библиотеки, чтобы повысить качество их работы в Groovy. Cначала я покажу, как можно использовать Groovy-класс ExpandoMetaClass для усовершенствования существующих классов, а затем - как использовать этот механизм для встраивания в Groovy библиотеки Functional Java.

Создание "открытых" классов с помощью класса ExpandoMetaClass

Ортогональность

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

Одна из наиболее впечатляющих возможностей Groovy – это «открытые классы» (функциональность, позволяющая «открыть» существующий класс, чтобы улучшить или сократить его функциональность). Этот подход отличается от наследования, при котором класс нового типа наследуется от существующего. Принцип "открытости" классов позволяет открыть такой класс как, например, String (напомню, что класс String является final-классом) и добавить в него новые методы. Библиотеки для unit-тестирования используют данную возможность, чтобы добавлять проверочные методы в класс Object, так что все классы приложения могут использовать эти методы.

В Groovy есть два подхода для создания "открытых" классов: категории и класс ExpandoMetaClass (см. раздел "Ресурсы"). Для представленного примера подойдет любой из этих подходов, но я выбрал ExpandoMetaClass из-за более простого синтаксиса.

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

Листинг 1. Законченный пример классификатора чисел на языке Groovy
class Classifier {
  def static isFactor(number, potential) {
    number % potential == 0;
  }

  def static factorsOf(number) {
    (1..number).findAll { i -> isFactor(number, i) }
  }

  def static sumOfFactors(number) {
    factorsOf(number).inject(0, {i, j -> i + j})
  }

  def static isPerfect(number) {
    sumOfFactors(number) == 2 * number
  }

  def static isAbundant(number) {
    sumOfFactors(number) > 2 * number
  }

  def static isDeficient(number) {
    sumOfFactors(number) < 2 * number
  }

  static def nextPerfectNumberFrom(n) {
    while (!isPerfect(++n));
    n
  }
}

Если у вас возникли какие-либо вопросы о реализации методов, используемых в данной версии, обратитесь к предыдущим статьям (в особенности "Связывание и композиция. Часть 2" и "Функциональные возможности Groovy. Часть 1"). Для использования методов данного класса я могу вызывать их в "стандартном" функциональном стиле: Classifier.isPerfect(7). Однако с помощью метапрограммирования я могу привязать эти методы непосредственно к классу Integer, что позволит мне напрямую "спросить" число, к какому типу оно относится.

Чтобы добавить эти методы к классу Integer, я воспользуюсь свойством metaClass данного класса, как показано в листинге 2 (в Groovy это свойство доступно для всех классов).

Листинг 2. Добавление классифицирующих методов к классу Integer
Integer.metaClass.isPerfect = {->
  Classifier.isPerfect(delegate)
}

Integer.metaClass.isAbundant = {->
  Classifier.isAbundant(delegate)
}

Integer.metaClass.isDeficient = {->
  Classifier.isDeficient(delegate)
}

Инициализация методов, основанных на метапрограммировании

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

В листинге 2 я добавляю к классу Integer три метода из класса Classifier, и все значения типа Integer в Groovy будут обладать этими методами (в Groovy отсутствуют примитивные типы данных, например, int, так как даже константы в Groovy используют Integer в качестве базового типа). Внутри блока кода, представляющего тело метода, я получаю доступ к предопределенному параметру delegate, который представляет собой значение объекта, вызвавшего метод класса Classifier.

После того как я проинициализировал мета-методы (см. вкладку "Инициализация методов, основанных на метапрограммировании"), я могу непосредственно вызвать метод числа, чтобы узнать его тип, как показано в листинге 3.

Листинг 3. Использование метапрограммирования для классификации чисел
@Test
void metaclass_classifiers() {
  def num = 28
  assertTrue num.isPerfect()
  assertTrue 7.isDeficient()
  assertTrue 6.isPerfect()
  assertTrue 12.isAbundant()
}

В листинге 3 показано, что новые методы работают как для переменных, так и для констант. После данного примера можно с легкостью добавить в класс Integer метод, возвращающий тип числа, например в виде enum-объекта.

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


Преобразование типов данных с помощью метапрограммирования

По сути, Groovy – это один из диалектов Java, так что подключение к нему сторонних библиотек, таких как Functional Java, является тривиальной задачей. Однако я могу ещё лучше интегрировать эти библиотеки с Groovy, если прибегну к помощи метапрограммирования для преобразования различных типов данных, чтобы обеспечить большую «прозрачность» при интеграции. В Groovy имеется встроенный механизм замыканий (на основе класса Closure). Так как в библиотеке Functional Java замыкания пока отсутствуют (поскольку она основана на синтаксисе Java 5), то её разработчики использовали параметризованные типы и общий класс F, содержащий метод f(). С помощью Groovy-класса ExpandoMetaClass я могу устранить различия между типами «метод» и «замыкание», создав методы, соединяющие их.

Я хочу усовершенствовать класс Stream из библиотеки Functional Java, предоставляющий абстракцию "бесконечного" списка. Я бы хотел передавать Groovy-замыкания вместо объектов типа F из арсенала Functional Java, поэтому я добавил в класс Stream перегруженный метод, чтобы преобразовать замыкания к методу f() класса F, как показано в листинге 4.

Листинг 4. Привязка типов данных с помощью класса ExpandoMetaClass
Stream.metaClass.filter = { c -> delegate.filter(c as fj.F) }
//    Stream.metaClass.filter = { Closure c -> delegate.filter(c as fj.F) }
Stream.metaClass.getAt = { n -> delegate.index(n) }
Stream.metaClass.getAt = { Range r -> r.collect { delegate.index(it) } }

На первой строке я создаю в классе Stream метод filter(), принимающий на вход замыкание (это параметр с). На второй, закомментированной, строке делается то же самое, но для добавляемого параметра указывается тип – Closure (это не влияет на выполнение кода в Groovy, но может оказаться предпочтительнее с точки зрения документации). В блоке кода вызывается метод filter(), уже существующий в классе Stream, при этом объект Groovy-типа Closure подставляется вместо класса fj.F из библиотеки Functional Java. Я воспользовался оператором as, доступным в Groovy, чтобы выполнить преобразование типов данных.

Оператор as преобразует замыкания в определения интерфейсов, позволяя сопоставлять методы замыкания с методами, объявленными в интерфейсе, как показано в листинге 5.

Листинг 5. Использование оператора as для "облегченного" итерирования
def h = [hasNext : { println "hasNext called"; return true}, 
         next : {println "next called"}] as Iterator
                                                          
h.hasNext()
h.next()
println "h instanceof Iterator? " + (h instanceof Iterator)

В листинге 5 я создаю хеш-таблицу, содержащую две пары ключ – значение. Имена элементов являются строками (в Groovy не требуется помещать значения ключей для хеш-таблиц в двойные кавычки, так как они по умолчанию являются строками), а значения элементов содержат блоки кода. Оператор as привязывает эту хеш-таблицу к интерфейсу Iterator, в котором также объявлены методы hasNext() и next(). После выполнения подобного преобразования я могу использовать свою хеш-таблицу как объект типа Iterator (результатом выполнения последней строки в листинге 5 будет true). В случаях, если мой интерфейс содержит единственный метод или если я хочу привязать все методы интерфейса к одному замыканию, я могу отказаться от хеш-таблицы и использовать оператор as для прямой привязки замыкания к функции. Возвращаясь обратно к первой строке в листинге 4, я привязал замыкание, переданное в качестве параметра, к одному методу класса F. В листинге 4 я также привязываю оба метода getAt(), один из которых принимает на вход число, а другой – объект типа Range, так как объекту filter для работы необходимы оба эти метода.

Используя усовершенствованный класс Stream, я могу работать с бесконечной последовательностью, как показано в листинге 6.

Листинг 6. Использование класса Stream, входящего в библиотеку Functional Java, в Groovy-приложении
@Test
void adding_methods_to_fj_classes() {

  def evens = Stream.range(0).filter { it % 2 == 0 }
  assertTrue(evens.take(5).asList() == [0, 2, 4, 6, 8])
  assertTrue(evens[3..6] == [6, 8, 10, 12])
}

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


Бесконечные потоки в Groovy

В предыдущей статье я показывал, как реализовать в Groovy бесконечный список с отложенным созданием элементов. Вместо того чтобы создавать его вручную, можно воспользоваться бесконечной последовательностью из библиотеки Functional Java.

Для создания "бесконечного" объекта Stream, выдающего совершенные числа, мне потребуется дополнительно привязать к замыканиям Groovy два метода класса Stream, как показано в листинге 7.

Листинг 7. Привязка двух методов, необходимых для создания «потока» совершенных чисел
Stream.metaClass.asList = { delegate.toCollection().asList() }
Stream.metaClass.static.cons = { head, closure -> delegate.cons(head, closure as fj.P1) }
// Stream.metaClass.static.cons = 
//  { head, Closure c -> delegate.cons(head, ['_1':c] as fj.P1)}

В листинге 7 я создаю преобразующий метод asList(), чтобы облегчить преобразование "потока" библиотеки Functional Java в обычный список. Также я перегружаю метод cons() класса Stream, который отвечает за создание нового списка. При создании бесконечного списка его структура обычно включает в себя первый элемент и замыкание с блоком кода, который при обращении генерирует следующий элемент, в качестве остальной части списка. Чтобы создать в Groovy бесконечный "поток" совершенных чисел, мне необходимо, чтобы инфраструктура Functional Java позволяла использовать Groovy-замыкание в качестве параметра для метода cons().

Если воспользоваться оператором as для привязки замыкания к интерфейсу, в котором объявлено несколько методов, это замыкание будет вызываться при вызове любого метода, имеющегося в интерфейсе. Подобная простая привязка подходит для большинства классов Functional Java. Однако для небольшого числа методов используется метод fj.P1, а не fj.F. Иногда в подобных ситуациях всё равно можно использовать простую привязку, так как нижележащие методы не используют других методов класса P1. В случаях, когда требуется большая точность, я могу воспользоваться более сложной привязкой (закомментированный пример её использования представлен в листинге 7), для которой необходимо создать хеш-таблицу с методом _1(), привязанным к замыканию. Хотя этот метод и выглядит довольно странно, но это обычный метод класса fj.P1, возвращающий первый элемент.

После того как я с помощью метапрограммирования привязал новые методы к классу Stream, можно воспользоваться классом Classifier из листинга 1, для создания бесконечного "потока" совершенных чисел, как показано в листинге 8.

Листинг 8. Бесконечный "поток" совершенных чисел, созданный с помощью Functional Java и Groovy
import static fj.data.Stream.cons
import static com.nealford.ft.metafunctionaljava.Classifier.nextPerfectNumberFrom

def perfectNumbers(num) {
  cons(nextPerfectNumberFrom(num), { perfectNumbers(nextPerfectNumberFrom(num))})
}

@Test
void infinite_stream_of_perfect_nums_using_functional_java() {
  assertEquals([6, 28, 496], perfectNumbers(1).take(3).asList())
}

Я использую static import для подключения метода cons() из библиотеки Functional Java и своего метода nextPerfectNumberFrom() из класса Classifier, чтобы сократить объём исходного кода. Метод perfectNumbers() возвращает бесконечную последовательность совершенных чисел, используя первое совершенное число после стартового значения в качестве первого элемента последовательности, а замыкание с блоком кода для вычисления совершенного числа в качестве второго элемента. Блок кода внутри замыкания возвращает бесконечную последовательность со следующим совершенным числом в качестве первого элемента, и замыкание опять используется в качестве второго элемента. В тесте я создаю последовательность совершенных чисел, начиная с 1, и извлекаю три следующих совершенных числа, чтобы проверить их совпадение с уже известными значениями.


Заключение

Когда разработчики думают о метапрограммировании, они обычно думают только о своём коде, а не о том, как можно улучшить сторонний код. Groovy позволяет мне добавлять новые методы не только к встроенным классам, таким как Integer, но и в сторонние библиотеки, например, Functional Java. Сочетание метапрограммирования и функционального программирования предоставляет огромные возможности при минимальном количестве кода, позволяя "прозрачно" соединять функциональность.

Хотя я могу вызывать классы Functional Java напрямую из Groovy, многие из компонентов библиотеки сильно уступают по изяществу настоящим замыканиям. Используя метапрограммирование, я могу привязываться к методам Functional Java так, чтобы они могли «понимать» и использовать удобные Groovy-структуры, соединяя тем самым сильные стороны обоих подходов. До тех пор пока в Java не появится встроенный тип «замыкание», разработчикам придется выполнять подобные привязки типов данных, используемых в различных языках (например, на уровне байт-кода Groovy-замыкания значительно отличаются от замыканий, используемых в Scala). После стандартной реализации замыканий в Java эти преобразования можно будет переложить на среду исполнения, и потребность в подобных привязках также исчезнет. Но до этого момента подобная возможность позволит создавать компактный и при этом мощный код.

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

Ресурсы

Научиться

  • Functional thinking: Functional features in Groovy, Part 2: оригинал статьи (EN).
  • The Productive Programmer (Neal Ford, O'Reilly Media, 2008): новая книга Нила Форда, в которой более подробно рассматриваются некоторые вопросы из данной серии статей.
  • Functional Java: инфраструктура Functional Java привносит в язык программирования Java многие возможности функциональных языков.
  • Practically Groovy: Metaprogramming with closures, ExpandoMetaClass, and categories (Скотт Дэвис, developerWorks, июнь 2009 г.) (EN): статья из серии Practically Groovy с дополнительной информацией о метапрограммировании.
  • Language designer's notebook: First, do no harm (Брайан Гетц, developerWorks, июль 2011 г.) (EN): статья, в которой рассматриваются особенности реализации лямбда-выражений или замыканий (lambda expressions - новая функциональность языка, которая разрабатывается для Java SE 8).
  • Посетите магазин книг, посвященных ИТ-технологиям и различным аспектам программирования.

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

Комментарии

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=828181
ArticleTitle=Функциональное мышление: Часть 2. Функциональные возможности Groovy
publish-date=07302012