Во многих публикациях мы исследовали концепцию функционального программирования на разных языках, которые являются предметом обсуждения F # и Scala. Однако, поскольку я занимаюсь Java на своем рабочем месте, изучение той же концепции кажется интересным, и глаза открываются, потому что прошло много времени с тех пор, как я в последний раз серьезно использовал Java.
Функции высшего порядка
Как объясняется здесь Функции высшего порядка, что это такое? Функции высшего порядка - это простые функции, которые могут принимать функции в качестве аргументов и возвращать другую функцию в качестве результатов.
В современной Java это легко сделать. Синтаксис не самый лучший, и поскольку нет вывода типа, мы должны явно объявить тип функции, который в Java означает какой-то интерфейс . Посмотрим как.
Во-первых, предположим, что у нас есть набор объектов, то есть набор собак, и у нас есть функция, которая действует на каждую собаку. Мы хотим иметь возможность вызывать эту функцию для каждого объекта (собаки).
Давайте посмотрим, как мы могли бы создать такую функцию.
@FunctionalInterface interface DogAge { Integer apply(Dog dog); } List<Integer> getAges(List<Dog> dogs, DogAge f) { List<Integer> ages = new ArrayList<>(); for (Dog dog : dogs) { ages.add(f.apply(dog)); } return ages; }
Мы определяем интерфейс, который, учитывая собаку, извлекает из нее некоторое целочисленное значение. Затем мы определяем функцию getAges, которая применяет переданную функцию (на данный момент interface) к каждой собаке.
Теперь нам нужно создать фактическую функцию, которую мы хотим применить к каждой собаке.
DogAge f = dog -> dog.getAge(); getAges(dogs, f);
Обратите внимание, что нам не нужно фактически определять реализацию DogAge, как это было сделано в более старой версии Java. Это будет следующий способ, но, пожалуйста, не используйте его больше.
DogAge dontUseMe = new DogAge() { @Override public Integer apply(Dog dog) { return dog.getAge(); } };
Первый фактически создается компилятором, когда он видит первый.
Мы можем пойти еще дальше и сделать следующее.
getAges(dogs, dog -> dog.getAge());
Здесь мы передаем функцию прямо в метод getAges.
Каким-то образом getAges является функцией более высокого порядка, поскольку она может принимать функции в качестве аргументов. Java сохраняет странную подпись, получая интерфейс, но я думаю, что это будет улучшено в будущих версиях языка.
Для сравнения, давайте определим getAges в Scala и посмотрим на различия. Кроме того, мы собираемся сразу изменить название функций, чтобы оно было более общим.
def extractStringFromDogs(dogs: List[Dog], f: Dog => String) = dogs.map(f)
в Java мы могли бы это сделать.
@FunctionalInterface interface DogMapper { String apply(Dog dog); } List<String> extractStringFromDogs(List<Dog> dogs, DogMapper f) { return dogs.stream().map(dog -> f.apply(dog)).collect(Collectors.toList); }
Бывает, что в Java уже есть структура, решающая ту же проблему. Это Функция ‹A, B›. Другими словами, мы могли бы это сделать.
List<String> extractStringFromDogs(List<Dog> dogs, Function<Dog, String> f) { return dogs.stream().map(dog -> f.apply(dog)).collect(Collectors.toList); } extractStringFromDogs(dogs, dog -> dog.getName());
А как насчет определения функций, которые фактически возвращают другие функции?
В Scala мы могли делать следующее.
scala> def sum(): (Int, Int) => Int = (a, b) => a + b sum: ()(Int, Int) => Int scala> sum() res1: (Int, Int) => Int = $$Lambda$1067/2036949810@715f45c6 scala> sum()(4,5) res2: Int = 9 scala> res1(2, 3) res3: Int = 5
Здесь sum возвращает функцию, которую можно сохранить и оценить в другое время. Это очень мощная и важная конструкция функциональных языков. Можем ли мы сделать то же самое на Java?
Начнем с определения нашего собственного типа функции (Функциональный интерфейс) для этой конкретной проблемы.
@FunctionalInterface interface TakeTwo { Integer apply(Integer a, Integer b); }
Как мы могли видеть, TakeTwo семантически совпадает с тем, что мы определили в Scala.
Теперь мы можем снова определить метод sum.
TakeTwo sum() { return (a, b) -> a + b; } TakeTwo mySum = sum(); Integer finalSum = mySum.apply(5, 6);
Это в точности то же самое, что и в Scala, только в Scala синтаксис краткий и есть не нужно определять функциональный интерфейс для использования в качестве типа функции. Да, результат тот же.
Опять же, нам фактически не нужно определять TakeTwo самостоятельно, поскольку эквивалентный интерфейс уже определен в Java. называется BiFunction. Используя его, мы могли бы записать сумму следующим образом.
BiFunction<Integer, Integer, Integer> sum() { return (a, b) -> a + b; }
Больше функциональных интерфейсов.
Чтобы поддержать усилия по функциональному программированию, Java включает в себя множество этих функциональных интерфейсов. Некоторые из них:
Потребитель
Java:
public interface Consumer<T> { void accept(T t); .... }
Scala
T => Unit
Предикат
Java
public interface Predicate<T> { boolean test(T t); ... }
Scala
T => boolean
Поставщик
Java
public interface Supplier<T> { T get(); }
Scala
:=> T
Функция
Java
public interface Function<T, R> { R apply(T t); ... }
Scala
T => R
BiFunction
Java
public interface BiFunction<T, U, R> { R apply(T t, U u); ... }
Scala
(T, U) => R
Это лишь некоторые из типов функций (Функциональные интерфейсы), которые можно найти в новых Java и их аналог в Scala. Обратите внимание, что в Scala нам не нужно определять для них какие-либо интерфейсы, у нас просто есть функции, и мы можем определять их по своему усмотрению.
Выводы
Почему-то Java определенно движется в сторону функционального программирования, и хотя синтаксис не самый удобный, результаты те же.
С другой стороны, синтаксис Scala гораздо более точен и лучше показывает цель без необходимости создания интерфейсов как типов функций.
Я просто надеюсь, что Java продолжит развиваться, сокращая многословие и добавляя новые функциональные конструкции, потому что в конечном итоге мы, инженеры, получаем от них реальную пользу.