Обработка исключений в Java в функциональном стиле. Часть 2.
В предыдущей статье была рассмотрена функциональная обработка исключений с помощью интерфейса Try<T>. Статья вызвала определенный интерес читателей и была отмечена в «Сезоне Java».
В данной статье автор продолжит тему и рассмотрит простую и «грамотную» (literate) обработку исключений при помощи функций высшего порядка без использования каких либо внешних зависимостей и сторонних библиотек.
Решение
Для начала перепишем пример из предыдущей статьи преобразования URL из строкового представления к объектам URL c использованием Optional.
public List<URL> urlList(String[] urls) { return Stream.of(urls) //Stream<String> .map(s -> { try { return Optional.of(new URL(s)); } catch (MalformedURLException me) { return Optional.empty(); } }) //Stream<Optional<>URL> .flatMap(Optional::stream) //Stream<URL>, filters empty optionals .toList(); }
Общая схема понятна, однако не хотелось бы писать подобный boilerplate код каждый раз когда мы встречается с функцией выбрасывающей проверяемые исключения.
И тут нам помогут следующие функции:
@FunctionalInterface interface CheckedFunction<T, R> { R apply(T t) throws Exception; } /** * Higher-order function to convert partial function T=>R to total function T=>Optional<R> * @param <T> function input parameter type * @param <R> function result type * @param func partial function T=>R that may throw checked exception * @return total function T => Optional<R> */ static <T, R> Function<T, Optional<R>> toOptional(CheckedFunction<T, R> func) { return param -> { try { return Optional.ofNullable(func.apply(param)); } catch (RuntimeException err) { throw err; //All runtime exceptions are treated as errors/bugs } catch (Exception e) { return Optional.empty(); } }; }
Дадим некоторые пояснения. CheckedFunction<T, R> это функция которая может выбросить исключение при преобразовании T => R. Подобные функции в терминах функционального программирования называются частичными (partial) функциями, потому что значение функции не определено при некоторых входных значениях параметра.
Функция toOptional(...) преобразует частичную (partial) функцию T => R в полную (total) функцию T => Optional. Подобного рода функции, которые принимают параметром и/или возвращают другую функцию, в терминах функционального программирования называются функциями высшего порядка (higher-order function).
С использованием новой функции код примет следующий опрятный вид:
public List<URL> urlList(String[] urls) { return Stream.of(urls) //Stream<String> .map(toOptional(URL::new)) //Stream<Optional<URL>> .flatMap(Optional::stream) //Stream<URL>, filters empty optionals .toList(); //List<URL> }
И теперь её можно применять везде где возможны проверяемые (checked) исключения.
List<Number> intList(String [] numbers) { NumberFormat format = NumberFormat.getInstance(); return Stream.of(numbers) //Stream<String> .map(toOptional(format::parse)) //Checked ParseException may happen here .flatMap(Optional::stream) //Stream<Number> .toList(); //List<Number> }
Улучшаем обработку исключений
При использовании Optional<T> пропадает информация о самом исключении. В крайнем случае исключение можно залогировать в теле функции toOptional, но мы найдем лучшее решение.
Нам нужен любой контейнер, который может содержать значение типа T либо само исключение. В терминах функционального программирования таким контейнером является Either<Exception, T>, но к сожаления класса Either<L,R> (как и класса Try<T>) нет в составе стандартной библиотеки Java.
Вы можете использовать любой подходящий контейнер, которым Вы обладаете. Я же в целях краткости буду использовать следующий
//Require Java 14+ record Result<T>(T result, Exception exception) { public boolean failed() {return exception != null;} public Stream<T> stream() {return failed() ? Stream.empty() : Stream.of(result);} }
Теперь наша функция высшего порядка получит имя toResult и будет выглядеть так:
static <T, R> Function<T, Result<R>> toResult(CheckedFunction<T, R> func) { return param -> { try { return new Result<>(func.apply(param), null); } catch (RuntimeException err) { throw err; } catch (Exception e) { return new Result<>(null, e); } }; }
А вот и применение новой функции toResult()
List<Number> intListWithResult(String [] numbers) { NumberFormat format = NumberFormat.getInstance(); return Stream.of(numbers) //Stream<String> .map(toResult(format::parse)) //Stream<Result<Number>>, ParseException may happen .peek(this::handleErr) //Stream<Result<Number>> .flatMap(Result::stream) //Stream<Number> .toList(); //List<Number> } void handleErr(Result r) { if (r.failed()) { System.out.println(r.exception()); } }
Теперь возможное проверяемое исключение сохраняется в контейнере Result и его можно обработать в потоке.
Выводы
Для простой и «грамотной» (literate) обработки проверяемых (checked) исключений в функциональном стиле без использования внешних зависимостей необходимо
-
Выбрать подходящий контейнер для хранения результата. В простейшем случае это может быть
Optional<T>. Лучше использовать контейнер который может хранить значение результата или перехваченное исключение. -
Написать функцию высшего порядка которая преобразует частичную функцию
T => R, которая может выбросить исключение, в полную функциюT => YourContainer<R>и применять ее в случае необходимости.
Ссылки
На github-e
Автор — Сергей А. Копылов
e-mail skopylov@gmail.com
ссылка на оригинал статьи https://habr.com/ru/post/687954/
Добавить комментарий