28 June 2011

Функциональное программирование в Java

Java
From Sandbox
Эта статья о:
  • О применении функционального стиля программирования в языке Java.
  • О некоторых базовых паттернах для работы с коллекциями данных из функционального программирования в примерах на Java.
  • Немного о библиотеке Google Collections.

Если вы программируете на языках Java, C#, C++, PHP, или любом другом ОО языке, хотели бы познакомиться с функциональным программированием, но не имеет возможности/желания изучать Haskell/Scala/Lisp/Python, — эта статья специально для вас.

Тем, кто знаком с функциональным программированием, но никогда не применял его в Java, думаю, это будет тоже интересно.


Вводим конструкцию «функция» в языке Java


Что такое функциональное программирование? Если в двух словах, то функциональное программирование — это программирование, в котором функции являются объектами, и их можно присваивать переменным, передавать в качестве аргументов другим функциям, возвращать в качестве результата от функций и т. п. Преимущества, которые раскрывает такая возможность, будут понятны чуть позже. Пока нам надо разобраться, как в Java можно использовать саму конструкцию «функция».

Как известно, в Java нету функций, там есть только классы, методы и объекты классов. Зато в Java есть анонимные классы, то есть классы без имени, которые можно объявлять прямо в коде любого метода. Этим мы и воспользуемся. Для начала объявим такой интерфейс:

public final interface Function<F, T> {
    T apply(F from);
}


Теперь в коде какого-нибудь метода мы можем объявить анонимную реализацию этого интерфейса:

public static void main() {
    // Объявляем "функцию", присваиваем ее переменной intToString.
    Function<Integer, String> intToString = new Function<Integer, String>() {
        @Override public String apply(Integer from) {
            return from.toString();
        }
    };

    intToString.apply(9000); // Вызываем нашу функцию. Получаем строку "9000".
}


Такую реализацию мы и будем называть «анонимной функцией». С точки зрения функционального программирования с ней можно делать все то же самое, что и с функцией из функциональных языков: присваивать переменным, передавать в качестве аргумента другим функциям(и методам классов), получать в качестве результата от функций(и методов классов).

Теперь можно перейти к изложению некоторых базовых паттернов функционального программирования.

Работа с коллекциями в функциональном стиле


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

public String joinNumbers(Collection<? extends Integer> 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<Integer, String> INT_TO_STRING = ... // Уже реализовали выше

// Берет поэлементно значения из коллекции from, преобразует их с помощью функции transformer
// и возвращает список результатов преобразования в том же порядке.
public static <F, T> List<T> map(Collection<F> from, Function<? super F,? extends T> transformer) {
    ArrayList<T> result = new ArrayList<T>();
    for (F element : from)
        result.add(transformer.apply(element));
    return result;
}

// Берет коллекцию произвольных элементов и конкатенирует их в строку
public static <T> String join(Collection<T> 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<? extends Integer> numbers) {
    return join(map(numbers, INT_TO_STRING), ", ");
}


Метод реализован ровно в одну простую строку.

Хотелось бы отметить несколько важных моментов:
  1. Методы map и join являются достаточно обобщенными, то есть их можно применять не только для решения данной задачи. Это значит, что их можно было бы выделить в некий утилитный класс, и использовать потом этот класс в разных частях проекта.
  2. Вместо класса Collection в методе map можно было бы передавать Iterable и возвращать новый Iterable, извлекая из переданной коллекции данные по мере обхода данных в возвращаемой коллекции, то есть извлекать элементы лениво, поэтапно, а не все сразу. Такая реализация, позволит, например, создавать цепочки преобразования данных, выделяя каждый этап преобразования в отдельную простую функцию, при этом эффективность алгоритма будет оставаться порядка O(n):
    map(map(numbers, MULTIPLY_X_2), INT_TO_STRING); // каждый элемент умножаем на два и приводим к строке.
  3. Создавая какой-нибудь класс, вы можете создавать для некоторых его методов статические поля, являющиеся функциями-обертками, делегирующими вызов apply на вызов соответствующего метода класса. Это позволит использовать «методы» объектов в функциональном стиле, например, в представленных выше конструкциях.


Работа с коллекциями с помощью Google Collections



Ребята из Google как раз создали удобную библиотеку с утилитными классами, позволяющую работать с коллекциями в Java в функциональном стиле. Вот некоторые из возможностей, которые она предоставляет:
  • interface Function<F, T>. Интерфейс, аналогичный приведенному мной выше.
  • 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 конечно есть еще много других полезных вещей как для функционального программирования, так и для работы с коллекциями в императивном стиле.

Ссылки



О чем хотелось бы рассказать еще


Дорогие друзья, если вам понравилась моя статья, я с удовольствием напишу еще что-нибудь интересное о применении функционального программирования в Java и других императивных языках. Вот некоторые из вещей, о которых есть желание рассказать, но нету возможности изложить сразу в одной статье:
  1. Мутабельные и иммутабельные замыкания.
  2. Pattern-matcher.
  3. Монады.
  4. Распараллеливание с использованием функционального подхода.
  5. Комбинаторы парсеров.

Буду рад услышать ваши комментарии и предложения.
Tags: Java функциональное программирование google collections
Hubs: Java
+74
39k 142
Comments 143
Ads