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

Введение в Прагматическую функциональную Java

Как должна выглядеть Java в 21 веке. С пометкой java, кодирование, стиль, новички.

ОБНОВЛЕНИЕ: добавлено важное примечание об инициализации по умолчанию.

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

Хотя этот стиль можно использовать даже с Java 8, с Java 11 он выглядит намного чище и лаконичнее. С Java 17 он становится еще более выразительным и извлекает выгоду из каждой новой функции языка Java.

Но PFJ – это не бесплатный обед, он требует значительных изменений в привычках и подходах разработчиков. Изменить привычки непросто, особенно трудно справиться с традиционными императивными привычками.

Стоит ли оно того? Определенно! Код PFJ лаконичен, выразителен и надежен, прост в чтении и обслуживании. В большинстве случаев, если код компилируется – он работает!

((Этот текст является неотъемлемой частью библиотеки Pragmatica ).

Элементы Прагматической Функциональной Java

PFJ является производным от замечательной Эффективной Java книги с некоторыми дополнительными концепциями и соглашениями, в частности, полученными из функционального программирования.

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

PFJ фокусируется на:

  • сокращение умственных затрат
  • повышение надежности кода
  • повышение долгосрочной ремонтопригодности
  • привлечение компилятора для написания правильного кода
  • сделать написание правильного кода легким и естественным; написание неправильного кода, хотя и возможно, должно потребовать усилий

Несмотря на амбициозные цели, существует только два ключевых правила PFJ:

  • Избегайте null как можно больше
  • Никаких исключений для бизнеса

Ниже каждое ключевое правило рассматривается более подробно:

Избегайте null, Насколько Это Возможно (правило ANAMAP)

Обнуляемость переменных является одним из Особых состояний . Они являются хорошо известным источником ошибок во время выполнения и шаблонного кода. Чтобы устранить эти проблемы и представить значения, которые могут отсутствовать, PFJ использует Option container. Это распространяется на все случаи, когда может появиться такое значение – возвращаемые значения, входные параметры или поля.

В некоторых случаях, например, по соображениям производительности или совместимости с существующими фреймворками, классы могут использовать null внутренне. Эти случаи должны быть четко задокументированы и невидимы для пользователей класса, т.Е. все API-интерфейсы классов должны использовать Option .

Такой подход имеет несколько преимуществ:

  • Обнуляемые переменные сразу видны в коде. Нет необходимости читать документацию/проверять исходный код/полагаться на аннотации.
  • Компилятор различает обнуляемые и ненулевые переменные и предотвращает неправильные присваивания между ними.
  • Весь шаблон, необходимый для проверок null , устранен.

Важный компонент правила карты ANA:

  • Нет инициализации по умолчанию. Каждая отдельная переменная должна быть явно инициализирована. Для этого есть две причины: сохранение контекста и исключение значений null .

Никаких бизнес-исключений (правило NBE)

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

Бизнес-исключения – это еще один случай Особых состояний . Для распространения и обработки ошибок бизнес-уровня PFJ использует Результат контейнер.

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

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

Правило Без бизнес-исключений обеспечивает следующие преимущества:

  • Методы, которые могут возвращать ошибку, сразу видны в коде. Нет необходимости читать документацию/проверять исходный код/анализировать дерево вызовов, чтобы проверить, какие исключения могут быть вызваны и при каких условиях.
  • Компилятор обеспечивает надлежащую обработку и распространение ошибок.
  • Практически нулевой шаблон для обработки и распространения ошибок.
  • Код может быть написан для сценария счастливого дня и ошибки обрабатываются в том месте, где это наиболее удобно – первоначальное намерение исключений, которое на самом деле так и не было достигнуто.
  • Код остается составным, его легко читать и рассуждать, никаких скрытых разрывов или неожиданных переходов в потоке выполнения – то, что вы читаете, – это то, что будет выполнено .

Преобразование Устаревшего Кода В Код В Стиле PFJ

Хорошо, ключевые правила кажутся хорошими и полезными, но как будет выглядеть реальный код?

Давайте начнем с довольно типичного серверного кода:

public interface UserRepository {
    User findById(User.Id userId);
}

public interface UserProfileRepository {
    UserProfile findById(User.Id userId);
}

public class UserService {
    private final UserRepository userRepository;
    private final UserProfileRepository userProfileRepository;

    public UserWithProfile getUserWithProfile(User.Id userId) {
        User user = userRepository.findById(userId);

        if (user == null) {
            throw UserNotFoundException("User with ID " + userId + " not found");
        }

        UserProfile details = userProfileRepository.findById(userId);

        return UserWithProfile.of(user, details == null 
            ? UserProfile.defaultDetails()
            : details);
    }
}

Интерфейсы в начале примера приведены для ясности контекста.

Основной интерес представляет метод get User With Profile . Давайте проанализируем это шаг за шагом.

  • Первый оператор извлекает переменную user из пользовательского репозитория.
  • Поскольку пользователь может отсутствовать в репозитории, переменная user может быть null . Следующая проверка null проверяет, так ли это, и выдает бизнес-исключение, если да.
  • Следующим шагом является извлечение сведений о профиле пользователя. Отсутствие подробностей не считается ошибкой. Вместо этого, когда сведения отсутствуют, для профиля используются значения по умолчанию.

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

Чтобы решить эту проблему, давайте применим правило A MAP к репозиториям:

public interface UserRepository {
    Option findById(User.Id userId);
}

public interface UserProfileRepository {
    Option findById(User.Id userId);
}

Теперь нет необходимости строить какие-либо догадки – API явно сообщает, что возвращаемое значение может отсутствовать.

Теперь давайте еще раз взглянем на метод get User With Profile . Второе, что следует отметить, – это то, что метод может возвращать значение или может выдавать исключение. Это бизнес-исключение, поэтому мы можем применить правило NBE . Основная цель изменения – сделать явным тот факт, что метод может возвращать значение ИЛИ ошибку:

    public Result getUserWithProfile(User.Id userId) {

Хорошо, теперь мы очистили API и можем начать изменять код. Первое изменение будет вызвано тем фактом, что UserRepository теперь возвращает Опция<Пользователь> :

    public Result getUserWithProfile(User.Id userId) {
        Option user = userRepository.findById(userId);
    }

Теперь нам нужно проверить, присутствует ли пользователь, и если нет, вернуть сообщение об ошибке. При традиционном императивном подходе код должен выглядеть следующим образом:

    public Result getUserWithProfile(User.Id userId) {
        Option user = userRepository.findById(userId);

        if (user.isEmpty()) {
            return Result.failure(Causes.cause("User with ID " + userId + " not found"));
        }
    }

Код выглядит не очень привлекательно, но и не хуже оригинала, так что давайте пока оставим его как есть.

Следующий шаг – попытаться преобразовать оставшиеся части кода:

    public Result getUserWithProfile(User.Id userId) {
        Option user = userRepository.findById(userId);

        if (user.isEmpty()) {
            return Result.failure(Causes.cause("User with ID " + userId + " not found"));
        }

        Option details = userProfileRepository.findById(userId);

    }

Здесь возникает подвох: детали и пользователь хранятся внутри Option контейнеров, поэтому для сборки Пользователь С Профилем нам нужно как-то извлекать значения. Здесь могут быть разные подходы, например, используйте Option.fold () метод. Результирующий код определенно не будет красивым и, скорее всего, будет нарушать правило ANAMAP .

Есть и другой подход – использовать тот факт, этот Option является контейнером со специальными свойствами . В частности, можно преобразовать значение внутри Option с помощью Option.map() и Опция.flatMap() методы. Кроме того, мы знаем, что значение details будет либо предоставлено репозиторием, либо заменено значением по умолчанию. Для этого мы можем использовать Option.или() метод для извлечения деталей из контейнера. Давайте попробуем эти подходы:

    public Result getUserWithProfile(User.Id userId) {
        Option user = userRepository.findById(userId);

        if (user.isEmpty()) {
            return Result.failure(Causes.cause("User with ID " + userId + " not found"));
        }

        UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());

        Option userWithProfile =  user.map(userValue -> UserWithProfile.of(userValue, details));

    }

Теперь нам нужно написать заключительный шаг – преобразование пользователь С профилем контейнер из Опции в Результат :

    public Result getUserWithProfile(User.Id userId) {
        Option user = userRepository.findById(userId);

        if (user.isEmpty()) {
            return Result.failure(Causes.cause("User with ID " + userId + " not found"));
        }

        UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());

        Option userWithProfile =  user.map(userValue -> UserWithProfile.of(userValue, details));

        return userWithProfile.toResult(Cause.cause(""));
    }

Давайте на мгновение оставим причину ошибки в операторе return пустой и снова посмотрим на код. Мы можем легко обнаружить проблему: мы определенно знаем, что пользователь с профилем всегда присутствует – случай, когда user отсутствует, уже обработан выше. Как мы можем это исправить?

Обратите внимание, что мы можем вызвать user.map() без проверки, присутствует пользователь или нет. Преобразование будет применено только в том случае, если user присутствует, и игнорируется, если нет. Таким образом, мы можем исключить if(user.isEmpty()) check. Давайте переместим извлечение details и преобразование User в User С профилем внутри лямбды, переданной в user.map() :

    public Result getUserWithProfile(User.Id userId) {
        Option userWithProfile = userRepository.findById(userId).map(userValue -> {
            UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());
            return UserWithProfile.of(userValue, details);
        });

        return userWithProfile.toResult(Cause.cause(""));
    }

Последнюю строку необходимо изменить сейчас, так как пользователь С профилем может отсутствовать. Ошибка будет такой же, как и в предыдущей версии, так как пользователь с профилем может отсутствовать только в том случае, если значение, возвращаемое UserRepository.findById(userId) отсутствует:

    public Result getUserWithProfile(User.Id userId) {
        Option userWithProfile = userRepository.findById(userId).map(userValue -> {
            UserProfile details = userProfileRepository.findById(userId).or(UserProfile.defaultDetails());
            return UserWithProfile.of(userValue, details);
        });

        return userWithProfile.toResult(Causes.cause("User with ID " + userId + " not found"));
    }

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

    public Result getUserWithProfile(User.Id userId) {
        return userRepository.findById(userId)
            .map(userValue -> UserWithProfile.of(userValue, userProfileRepository.findById(userId)
                                                                                 .or(UserProfile.defaultDetails())))
            .toResult(Causes.cause("User with ID " + userId + " not found"));
    }

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

Давайте проанализируем полученный код.

  • Код более лаконичен и написан для сценария счастливого дня , без явных ошибок или проверок null , без отвлечения от бизнес-логики
  • Нет простого способа пропустить или избежать ошибок или проверок null , написание правильного и надежного кода является простым и естественным.

Менее очевидные наблюдения:

  • Все типы автоматически выводятся. Это упрощает рефакторинг и устраняет ненужный беспорядок. При необходимости типы все еще могут быть добавлены.
  • Если в какой-то момент репозитории начнут возвращать Result вместо Option , код останется неизменным, за исключением того, что последнее преобразование ( toResult ) будет удалено.
  • Помимо замены троичного оператора методом Option.or() , результирующий код выглядит очень похоже на то, как если бы мы переместили код из исходного оператора return внутри лямбды, переданной в метод map() .

Последнее наблюдение очень полезно для того, чтобы начать удобно писать (чтение обычно не является проблемой) код в стиле PFJ. Его можно переписать в следующее эмпирическое правило: ищите значение с правой стороны . Просто сравните:

 User user = userRepository.findById(userId);    // <-- value is on the left side of the expression

и

 return userRepository.findById(userId)
                      .map(user -> ...); // <-- value is on the right side of the expression

Это полезное наблюдение помогает при переходе от устаревшего императивного стиля кода к PFJ.

Взаимодействие С Устаревшим Кодом

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

Вызов устаревшего кода

Есть две основные проблемы с вызовом устаревшего кода. Каждый из них связан с нарушением соответствующего правила PFJ:

Обработка Бизнес-Исключений

Результат содержит вспомогательный метод, называемый lift() , который охватывает большинство вариантов использования. Сигнатура метода выглядит так:

static  Result lift(FN1 exceptionMapper, ThrowingSupplier supplier)

Первый параметр – это функция, которая преобразует исключение в экземпляр Причина (который, в свою очередь, используется для создания экземпляров Result в случаях сбоя).

Второй параметр – это лямбда-выражение, которое переносит вызов в фактический код, который необходимо сделать PFJ-совместимым.

Самая простая из возможных функций, которая преобразует исключение в экземпляр Причина указана в Причинах | служебного класса: из Throwable() . Вместе с Result.lift() их можно использовать следующим образом:

public static Result createURI(String uri) {
    return Result.lift(Causes::fromThrowable, () -> URI.create(uri));
}

Обработка Возвратов нулевого Значения

Этот случай довольно прост – если API может возвращать null , просто оберните его в Option с помощью метода Option.option() .

Предоставление устаревшего API

Иногда необходимо разрешить устаревший код вызова кода, написанный в стиле PFJ. В частности, это часто происходит, когда какая-то меньшая подсистема преобразуется в стиль PJ, но остальная часть системы остается написанной в старом стиле, и API необходимо сохранить.

Самый удобный способ сделать это – разделить реализацию на две части – API в стиле PFJ и адаптер, который использует только новый API для старого API. Здесь может быть очень полезен простой вспомогательный метод, подобный показанному ниже:

public static  T unwrap(Result value) {
    return value.fold(
        cause -> { throw new IllegalStateException(cause.message()); },
        content -> content
    );
}

В Result нет готового к использованию вспомогательного метода по следующим причинам:

  • могут быть разные варианты использования, и могут создаваться разные типы исключений (отмеченные и непроверенные).
  • преобразование Cause в различные конкретные исключения в значительной степени зависит от конкретного варианта использования.

Управление областями Переменных

Этот раздел будет посвящен различным практическим случаям, которые возникают при написании кода в стиле PFJ.

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

Вложенные области

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

Давайте взглянем на следующий императивный код:

var value1 = function1(...);                    // function1() may throw exception
var value2 = function2(value1, ...);            // function2() may throw exception
var value3 = function3(value1, value2, ...);    // function3() may throw exception

Переменная value1 должна быть доступна для вызова функции 2() и функции 3() . Это означает, что последующее прямое преобразование в стиль PFJ не будет работать:

   function1(...)
       .flatMap(value1 -> function2(value1, ...))
       .flatMap(value2 -> function3(value1, value2, ...)); // <-- ERROR, value1 is not accessible!  

Чтобы сохранить значение доступным, нам нужно использовать вложенную область , т.Е. вложенные вызовы следующим образом:

   function1(...)
       .flatMap(value1 -> function2(value1, ...)
           .flatMap(value2 -> function3(value1, value2, ...)));   

Второй вызов flatMap() выполняется для значения, возвращаемого функцией 2 , а не к значению, возвращаемому first flatMap() . Таким образом, мы сохраняем значение 1 в пределах области видимости и делаем его доступным для функции 3 .

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

Параллельные области видимости

Другим часто наблюдаемым случаем является необходимость вычисления/извлечения нескольких независимых значений, а затем выполнения вызова или создания объекта. Давайте взглянем на приведенный ниже пример:

var value1 = function1(...);    // function1() may throw exception
var value2 = function2(...);    // function2() may throw exception
var value3 = function3(...);    // function3() may throw exception

return new MyObject(value1, value2, value3);

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

Для таких случаев Опция и Result предоставляют набор методов all() . Эти методы выполняют “параллельное” вычисление всех значений и возвращают выделенную версию MapperX<...> интерфейса. Этот интерфейс имеет только три метода – id() , map() и flatMap() ./| Карта() и flatMap() методы работают точно так же, как соответствующие методы в Option и Result , за исключением того, что они принимают лямбды с разным количеством параметров. Давайте посмотрим, как это работает на практике, и преобразуем приведенный выше императивный код в стиль PFJ:

return Result.all(
          function1(...), 
          function2(...), 
          function3(...)
        ).map(MyObject::new);

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

Альтернативные Области применения

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

Давайте взглянем на следующий императивный код:

MyType value;

try {
    value = function1(...);
} catch (MyException e1) {
    try {
        value = function2(...);    
    } catch(MyException e2) {
        try {
            value = function3(...);
        } catch(MyException e3) {
            ... // repeat as many times as there are alternatives 
        }
    }
}

Код несколько надуман, потому что вложенные случаи обычно скрыты внутри других методов. Тем не менее, общая логика далеко не проста, главным образом потому, что помимо выбора значения нам также необходимо обрабатывать ошибки. Обработка ошибок загромождает код и делает первоначальное намерение – выбрать первую доступную альтернативу – скрытым внутри обработки ошибок.

Трансформация в стиль PFJ делает намерение кристально ясным:

var value = Result.any(
        function1(...),
        function2(...),
        function3(...)
    );

К сожалению, здесь есть одно важное отличие: исходный императивный код вычисляет вторую и последующие альтернативы только при необходимости. В некоторых случаях это не является проблемой, но во многих случаях это крайне нежелательно. К счастью, существует ленивая версия Result.any() . Используя его, мы можем переписать код следующим образом:

var value = Result.any(
        function1(...),
        () -> function2(...),
        () -> function3(...)
    );

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

Краткий технический обзор опции и результата

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

Option – это довольно простая реализация Option/Optional/Maybe монады.

Результат является намеренно упрощенной и специализированной версией Либо : тип left исправлен и должен реализовывать Причина интерфейс. Специализация делает API очень похожим на Option и устраняет множество ненужных вводов текста ценой потери универсальности и общности.

Эта конкретная реализация сосредоточена на двух вещах:

  • Совместимость друг с другом и существующими классами JDK, такими как Необязательно и Поток
  • API, предназначенный для четкого выражения намерений

Последнее утверждение заслуживает более подробного объяснения.

Каждый контейнер имеет несколько методов core :

  • заводской способ (ы)
  • map() метод преобразования, который преобразует значение, но не изменяет специальное состояние : присутствует Опция остается присутствующей, успех Результат остается успешным.
  • flatMap() метод преобразования, который, помимо преобразования, также может изменять специальное состояние : преобразовать существующий Параметр в пустой или успешный Привести к сбою.
  • метод fold() , который обрабатывает оба случая (присутствует/пусто для Option и успех/неудача для Result ) одновременно.

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

Опция имеет следующие методы для побочных эффектов :

Option whenPresent(Consumer consumer);
Option whenEmpty(Runnable action);
Option apply(Runnable emptyValConsumer, Consumer nonEmptyValConsumer);

Результат имеет следующие методы для побочных эффектов :

Result onSuccess(Consumer consumer);
Result onSuccessDo(Runnable action);
Result onFailure(Consumer consumer);
Result onFailureDo(Runnable action);
Result apply(Consumer failureConsumer, Consumer successConsumer);

Эти методы дают читателю подсказки о том, что код имеет дело с побочными эффектами, а не с преобразованиями.

Другие Полезные Инструменты

Кроме того Опция и Результат , PFJ использует некоторые другие классы общего назначения. Ниже каждый из них описан более подробно.

Функции

JDK предоставил множество полезных функциональных интерфейсов. К сожалению, функциональные интерфейсы для функций общего назначения ограничены только двумя версиями: single parameter Функция R> и два параметра R> и два параметра

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

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

Кортежи

Кортежи – это специальный контейнер, который можно использовать для хранения нескольких значений разных типов в одной переменной. В отличие от классов или записей, значения, хранящиеся внутри, не имеют имен. Это делает их незаменимым инструментом для захвата произвольного набора значений при сохранении типов. Отличным примером такого варианта использования является реализация Result.all() и Option.all() наборы методов.

В некотором смысле кортежи можно рассматривать как замороженный набор параметров , подготовленный для вызова функции. С этой точки зрения решение сделать внутренние значения кортежа доступными только с помощью метода map() звучит разумно. Тем не менее, кортеж с 2 параметрами имеет дополнительные аксессуары, которые позволяют использовать Tuple2 в качестве замены для различных Пара реализации.

PFJ использует согласованный набор реализаций кортежей со значениями от 0 до 9. Кортежи со значениями 0 и 1 предоставляются для обеспечения согласованности.

Вывод

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

  • PFJ включает компилятор Java, помогающий писать надежный код:
    • Код, который компилируется, обычно работает
    • Многие ошибки перешли с времени выполнения на время компиляции
    • Некоторые классы ошибок, такие как NullPointerException или необработанные исключения, практически устранены
  • PFJ значительно сокращает объем шаблонного кода, связанного с распространением и обработкой ошибок, а также проверками null
  • PFJ фокусируется на четком выражении намерений и снижении умственных затрат

Оригинал: “https://dev.to/siy/introduction-to-pragmatic-functional-java-142m”