Рубрики
Без рубрики

7 Методов функционального программирования на Java – Учебник

Концепции функционального программирования на Java для начинающих. С пометкой java, функционал, новички, программирование.

функциональное программирование (Серия из 4 частей)

Первоначально опубликовано на deepu.tech .

Вокруг функционального программирования (FP) много шумихи, и этим занимаются многие крутые ребята но это не серебряная пуля. Как и другие парадигмы/стили программирования, функциональное программирование также имеет свои плюсы и минусы, и можно предпочесть одну парадигму другой. Если вы разработчик Java и хотите заняться функциональным программированием, не волнуйтесь, вам не нужно изучать языки, ориентированные на функциональное программирование, такие как Haskell или Clojure (или даже Scala или JavaScript, хотя они не являются чисто функциональными языками программирования), так как Java вас охватила, и этот пост для вас.

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

Что такое функциональное программирование?

Согласно Википедии,

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

Следовательно, в функциональном программировании существуют два очень важных правила

  • Никаких мутаций данных : Это означает, что объект данных не следует изменять после его создания.
  • Нет неявного состояния : Скрыто/Следует избегать неявного состояния. В функциональном программировании состояние не устраняется, вместо этого оно делается видимым и явным

Это означает:

  • Никаких побочных эффектов : Функция или операция не должны изменять какое-либо состояние за пределами своей функциональной области. Т.е. функция должна возвращать значение только вызывающему и не должна влиять на какое-либо внешнее состояние. Это означает, что программы легче понять.
  • Только чистые функции : Функциональный код является идемпотентным. Функция должна возвращать значения только на основе переданных аргументов и не должна влиять (побочный эффект) или зависеть от глобального состояния. Такие функции всегда дают один и тот же результат для одних и тех же аргументов.

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

Использование функционального программирования не означает, что это все или ничего, вы всегда можете использовать концепции функционального программирования в дополнение к объектно-ориентированным концепциям, особенно в Java. Преимущества функционального программирования могут быть использованы всякий раз, когда это возможно, независимо от используемой парадигмы или языка. И это именно то, что мы сейчас увидим.

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

Итак, давайте посмотрим, как мы можем применить некоторые из описанных выше концепций функционального программирования в Java. Мы будем использовать Java 11, так как в настоящее время это версия LTS.

Функции первого класса и более высокого порядка

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

Наиболее близкими к первоклассным функциям в Java являются Лямбда-выражения . Существуют также некоторые встроенные функциональные интерфейсы, такие как Функция , Потребитель , Предикат , Поставщик и так далее в пакете java.util.function , который можно использовать для функционального программирования.

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

Это не самый красивый способ выполнения функций более высокого порядка, но так оно и есть в Java, и это не так уж плохо, IMO.

public class HocSample {
    public static void main(String[] args) {
        var list = Arrays.asList("Orange", "Apple", "Banana", "Grape");

        // we are passing an array and an anonymous inner class instance of FnFactory as arguments to mapForEach method.
        var out = mapForEach(list, new FnFactory() {
            @Override
            public Object execute(final String it) {
                return it.length();
            }
        });
        System.out.println(out); // [6, 5, 6, 5]
    }

    // The method takes an array and an instance of FnFactory as arguments
    static  ArrayList mapForEach(List arr, FnFactory fn) {
        var newArray = new ArrayList();
        // We are executing the method from the FnFactory instance
        arr.forEach(t -> newArray.add(fn.execute(t)));
        return newArray;
    }

    @FunctionalInterface // this doesn't do anything it is just informative.
    public interface FnFactory {
        // The interface defines the contract for the anonymous class
        S execute(T it);
    }
}

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

public class HocSample {
    public static void main(String[] args) {
        var list = Arrays.asList("Orange", "Apple", "Banana", "Grape");
        // we are passing the array and a lambda expression as arguments to mapForEach method.
        var out = mapForEach(list, it -> it.length());
        // This can be further simplified to "mapForEach(list, String::length);", I'm writing the expanded version for readability
        System.out.println(out); // [6, 5, 6, 5]
    }

    // The method takes an array and an instance of Function as arguments (we have replaced the custom interface with the built-in one)
    static  ArrayList mapForEach(List arr, Function fn) {
        var newArray = new ArrayList();
        // We are executing the method from the Function instance
        arr.forEach(t -> newArray.add(fn.apply(t)));
        return newArray;
    }
}

Используя эти концепции наряду с лямбда-выражениями, мы можем писать замыкания и каррирование, как показано ниже

public class ClosureSample {
    // this is a higher-order-function that returns an instance of Function interface
    Function add(final int x) {
        // this is a closure, i.e, a variable holding an anonymous inner class instance of the Function interface
        // which uses variables from the outer scope
        var partial = new Function() {
            @Override
            public Integer apply(Integer y) {
                // variable x is obtained from the outer scope of this method which is declared as final
                return x + y;
            }
        };
        // The closure function instance is returned here
        return partial;
    }

    public static void main(String[] args) {
        ClosureSample sample = new ClosureSample();

        // we are currying the add method to create more variations
        var add10 = sample.add(10);
        var add20 = sample.add(20);
        var add30 = sample.add(30);

        System.out.println(add10.apply(5)); // 15
        System.out.println(add20.apply(5)); // 25
        System.out.println(add30.apply(5)); // 35
    }
}

Мы можем упростить это еще больше с помощью лямбда-выражений, как показано ниже

public class ClosureSample {
    // this is a higher-order-function that returns an instance of Function interface
    Function add(final int x) {
        // The lambda expression is returned here as closure
        // variable x is obtained from the outer scope of this method which is declared as final
        return y -> x + y;
    }

    public static void main(String[] args) {
        ClosureSample sample = new ClosureSample();

        // we are currying the add method to create more variations
        var add10 = sample.add(10);
        var add20 = sample.add(20);
        var add30 = sample.add(30);

        System.out.println(add10.apply(5));
        System.out.println(add20.apply(5));
        System.out.println(add30.apply(5));
    }
}

В Java также есть множество встроенных функций более высокого порядка, например, вот метод сортировки из java.util. Коллекции

var list = Arrays.asList("Apple", "Orange", "Banana", "Grape");

// This can be simplified as "Collections.sort(list, Comparator.naturalOrder());", I'm writing the expanded version for readability
Collections.sort(list, (String a, String b) -> {
    return a.compareTo(b);
});

System.out.println(list); // [Apple, Banana, Grape, Orange]

API Java stream также предоставляет множество интересных функций более высокого порядка, таких как forEach, map и так далее.

Чистые функции

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

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

public static int sum(int a, int b) {
    return a + b;
}

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

static Map map = new HashMap();

public static int sum(int a, int b) {
    var c = a + b;
    map.put(a + "+" + b, c);
    return c;
}

Поэтому старайтесь, чтобы ваши функции были чистыми и простыми.

Рекурсия

Функциональное программирование предпочитает рекурсию циклу. В Java это может быть достигнуто либо с помощью stream API, либо путем написания рекурсивных функций. Давайте рассмотрим пример вычисления факториала числа.

Я также провел тест на них, используя JMH , и упомянул наносекунды/операцию ниже

В традиционном итеративном подходе:

public class FactorialSample {
    // benchmark 9.645 ns/op
    static long factorial(long num) {
        long result = 1;
        for (; num > 0; num--) {
            result *= num;
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.println(factorial(20)); // 2432902008176640000
    }
}

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

public class FactorialSample {
    // benchmark 19.567 ns/op
    static long factorialRec(long num) {
        return num == 1 ? 1 : num * factorialRec(num - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorialRec(20)); // 2432902008176640000
    }
}

Недостатком рекурсивного подхода является то, что в большинстве случаев он будет медленнее по сравнению с итеративным подходом (преимущество, к которому мы стремимся, заключается в простоте и удобочитаемости кода) и может привести к ошибкам переполнения стека, поскольку каждый вызов функции необходимо сохранять в виде фрейма в стек. Чтобы избежать этого, предпочтительна хвостовая рекурсия, особенно когда рекурсия выполняется слишком много раз. В хвостовой рекурсии рекурсивный вызов является последним, что выполняется функцией, и, следовательно, фрейм стека функций не должен сохраняться компилятором. Большинство компиляторов могут оптимизировать код хвостовой рекурсии так же, как оптимизируется итеративный код, что позволяет избежать снижения производительности. Компилятор Java, к сожалению, не выполняет эту оптимизацию:(

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

public class FactorialSample {
    // benchmark 16.701 ns/op
    static long factorialTailRec(long num) {
        return factorial(1, num);
    }

    static long factorial(long accumulator, long val) {
        return val == 1 ? accumulator : factorial(accumulator * val, val - 1);
    }

    public static void main(String[] args) {
        System.out.println(factorialTailRec(20)); // 2432902008176640000
    }
}

Мы также можем использовать библиотеку Java stream для рекурсии, но на данный момент она медленнее обычной рекурсии.

public class FactorialSample {
    // benchmark 59.565 ns/op
    static long factorialStream(long num) {
        return LongStream.rangeClosed(1, num)
                .reduce(1, (n1, n2) -> n1 * n2);
    }

    public static void main(String[] args) {
        System.out.println(factorialStream(20)); // 2432902008176640000
    }
}

Рассмотрите возможность использования stream API или рекурсии при написании кода Java для удобства чтения и неизменяемости, но если производительность критична или если количество итераций будет огромным, используйте стандартные циклы.

Ленивая оценка

Ленивая оценка или нестрогая оценка – это процесс задержки оценки выражения до тех пор, пока оно не понадобится. В общем, Java выполняет строгую оценку, но для таких операндов, как && , и ?:

Возьмем этот пример, где Java охотно оценивает все.

public class EagerSample {
    public static void main(String[] args) {
        System.out.println(addOrMultiply(true, add(4), multiply(4))); // 8
        System.out.println(addOrMultiply(false, add(4), multiply(4))); // 16
    }

    static int add(int x) {
        System.out.println("executing add"); // this is printed since the functions are evaluated first
        return x + x;
    }

    static int multiply(int x) {
        System.out.println("executing multiply"); // this is printed since the functions are evaluated first
        return x * x;
    }

    static int addOrMultiply(boolean add, int onAdd, int onMultiply) {
        return (add) ? onAdd : onMultiply;
    }
}

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

executing add
executing multiply
8
executing add
executing multiply
16

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

public class LazySample {
    public static void main(String[] args) {
        // This is a lambda expression behaving as a closure
        UnaryOperator add = t -> {
            System.out.println("executing add");
            return t + t;
        };
        // This is a lambda expression behaving as a closure
        UnaryOperator multiply = t -> {
            System.out.println("executing multiply");
            return t * t;
        };
        // Lambda closures are passed instead of plain functions
        System.out.println(addOrMultiply(true, add, multiply, 4));
        System.out.println(addOrMultiply(false, add, multiply, 4));
    }

    // This is a higher-order-function
    static  R addOrMultiply(
            boolean add, Function onAdd,
            Function onMultiply, T t
    ) {
        // Java evaluates expressions on ?: lazily hence only the required method is executed
        return (add ? onAdd.apply(t) : onMultiply.apply(t));
    }
}

Это выводит следующее, и мы можем видеть чтобы выполнялись только необходимые функции

executing add
8
executing multiply
16

Система типов

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

Ссылочная прозрачность

Из Википедии:

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

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

Например, приведенное ниже приведет к ошибке при компиляции

final var list = Arrays.asList("Apple", "Orange", "Banana", "Grape");

list = Arrays.asList("Earth", "Saturn");

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

final var list = new ArrayList<>();

list.add("Test");
list.add("Test 2");

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

Структуры данных

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

Вывод

Это всего лишь введение для тех, кто пытается применить некоторые методы функционального программирования на Java. В Java можно сделать гораздо больше, и Java 8 добавила множество API, облегчающих функциональное программирование на Java, таких как stream API, дополнительный интерфейс, функциональные интерфейсы и так далее. Как я уже говорил ранее, функциональное программирование – это не серебряная пуля, но оно предлагает множество полезных методов для более понятного, ремонтопригодного и тестируемого кода. Он может прекрасно сосуществовать с императивным и объектно-ориентированным стилями программирования. На самом деле, мы все должны использовать все самое лучшее.

Вот видео со встречи, которое я представил, посвященное этому контенту.

Я надеюсь, что вы найдете это полезным. Если у вас есть какие-либо вопросы или вы думаете, что я что-то пропустил, пожалуйста, добавьте комментарий.

Если вам понравилась эта статья, пожалуйста, оставьте лайк или комментарий.

Вы можете следовать за мной по Твиттер и LinkedIn .

функциональное программирование (Серия из 4 частей)

Оригинал: “https://dev.to/deepu105/functional-programming-in-java-a-primer-13nb”