Лучший подход к обработке исключений функциональным способом

Исключения, особенно проверенные, могут серьезно прервать поток логики программы, когда идиома FP используется в Java 8. Вот произвольный пример:

String s1 = "oeu", s2 = "2";
Stream.of(s1, s2).forEach(s -> 
    System.out.println(Optional.of(s).map(Integer::parseInt).get()));

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

Stream.of(s1, s2).forEach(s -> 
   System.out.println(Optional.of(s)
                              .map(Integer::parseInt)
                              .orElse(-1)));

Конечно, это все еще не получается, потому что Optional обрабатывает только null s. Я хотел бы что-то следующее:

Stream.of(s1, s2).forEach(s ->
    System.out.println(
        Exceptional.of(s)
                   .map(Integer::parseInt)
                   .handle(NumberFormatException.class, swallow())
                   .orElse(-1)));

Примечание.. Это вопрос, ответивший на вопрос.

Ответы

Ответ 1

Ниже представлен полный код класса Exceptional. Он имеет довольно большой API, который является чистым расширением Optional API, поэтому он может быть заменой для него в любом существующем коде, за исключением того, что он не является подтипом конечного Optional класса. Класс можно рассматривать как находящийся в том же соотношении с Try monad как Optional с монадой Maybe: он черпает вдохновение от него, но адаптирован к идиоме Java (например, фактически бросает исключения, даже из нетерминальных операций),

Вот некоторые ключевые рекомендации, за которыми следует класс:

  • в отличие от монадического подхода, не игнорирует механизм исключения Java;

  • вместо этого он устраняет несоответствие импеданса между исключениями и функциями более высокого порядка;

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

Класс пытается охватить все типичные способы обработки исключения:

  • recover с некоторым кодом обработки, который обеспечивает заменяющее значение;
  • flatRecover который, аналогично flatMap, позволяет возвратить новый Exceptional экземпляр, который будет развернут, и состояние текущего экземпляра соответствующим образом обновлено;
  • propagate исключение, выкидывая его из выражения Exceptional и делая вызов propagate объявлением этого типа исключения;
  • propagate его после переноса на другое исключение (перевести его);
  • handle его, что приводит к пустому Exceptional;
  • как особый случай обработки, swallow его пустым блоком обработчика.

Метод propagate позволяет выборочно выбирать, какие проверенные исключения он хочет выставить из своего кода. Исключения, которые остаются необработанными во время вызова терминальной операции (например, get), будут скрытно выбрасываться без объявления. Это часто рассматривается как продвинутый и опасный подход, но тем не менее часто используется как способ немного облегчить неприятность проверенных исключений в сочетании с формами лямбды, которые не объявляют их. Exceptional класс надеется предложить более чистую и более избирательную альтернативу скрытому броску.


/*
 * Copyright (c) 2015, Marko Topolnik. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public final class Exceptional<T>
{
  private final T value;
  private final Throwable exception;

  private Exceptional(T value, Throwable exc) {
    this.value = value;
    this.exception = exc;
  }

  public static <T> Exceptional<T> empty() {
    return new Exceptional<>(null, null);
  }

  public static <T> Exceptional<T> ofNullable(T value) {
    return value != null ? of(value) : empty();
  }

  public static <T> Exceptional<T> of(T value) {
    return new Exceptional<>(Objects.requireNonNull(value), null);
  }

  public static <T> Exceptional<T> ofNullableException(Throwable exception) {
    return exception != null? new Exceptional<>(null, exception) : empty();
  }

  public static <T> Exceptional<T> ofException(Throwable exception) {
    return new Exceptional<>(null, Objects.requireNonNull(exception));
  }

  public static <T> Exceptional<T> from(TrySupplier<T> supplier) {
    try {
      return ofNullable(supplier.tryGet());
    } catch (Throwable t) {
      return new Exceptional<>(null, t);
    }
  }

  public static Exceptional<Void> fromVoid(TryRunnable task) {
    try {
      task.run();
      return new Exceptional<>(null, null);
    } catch (Throwable t) {
      return new Exceptional<>(null, t);
    }
  }

  public static <E extends Throwable> Consumer<? super E> swallow() {
    return e -> {};
  }

  public T get() {
    if (value != null) return value;
    if (exception != null) sneakyThrow(exception);
    throw new NoSuchElementException("No value present");
  }

  public T orElse(T other) {
    if (value != null) return value;
    if (exception != null) sneakyThrow(exception);
    return other;
  }

  public T orElseGet(Supplier<? extends T> other) {
    if (value != null) return value;
    if (exception != null) sneakyThrow(exception);
    return other.get();
  }

  public Stream<T> stream() { 
      return value == null ? Stream.empty() : Stream.of(value); 
  }

  public<U> Exceptional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (value == null) return new Exceptional<>(null, exception);
    final U u;
    try {
      u = mapper.apply(value);
    } catch (Throwable exc) {
      return new Exceptional<>(null, exc);
    }
    return ofNullable(u);
  }

  public<U> Exceptional<U> flatMap(Function<? super T, Exceptional<U>> mapper) {
    Objects.requireNonNull(mapper);
    return value != null ? Objects.requireNonNull(mapper.apply(value)) : empty();
  }

  public Exceptional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (value == null) return this;
    final boolean b;
    try {
      b = predicate.test(value);
    } catch (Throwable t) {
      return ofException(t);
    }
    return b ? this : empty();
  }

  public <X extends Throwable> Exceptional<T> recover(
      Class<? extends X> excType, Function<? super X, T> mapper)
  {
    Objects.requireNonNull(mapper);
    return excType.isInstance(exception) ? ofNullable(mapper.apply(excType.cast(exception))) : this;
  }

  public <X extends Throwable> Exceptional<T> recover(
      Iterable<Class<? extends X>> excTypes, Function<? super X, T> mapper)
  {
    Objects.requireNonNull(mapper);
    for (Class<? extends X> excType : excTypes)
      if (excType.isInstance(exception))
        return ofNullable(mapper.apply(excType.cast(exception)));
    return this;
  }

  public <X extends Throwable> Exceptional<T> flatRecover(
      Class<? extends X> excType, Function<? super X, Exceptional<T>> mapper)
  {
    Objects.requireNonNull(mapper);
    return excType.isInstance(exception) ? Objects.requireNonNull(mapper.apply(excType.cast(exception))) : this;
  }

  public <X extends Throwable> Exceptional<T> flatRecover(
      Iterable<Class<? extends X>> excTypes, Function<? super X, Exceptional<T>> mapper)
  {
    Objects.requireNonNull(mapper);
    for (Class<? extends X> c : excTypes)
      if (c.isInstance(exception))
        return Objects.requireNonNull(mapper.apply(c.cast(exception)));
    return this;
  }

  public <E extends Throwable> Exceptional<T> propagate(Class<E> excType) throws E {
    if (excType.isInstance(exception))
      throw excType.cast(exception);
    return this;
  }

  public <E extends Throwable> Exceptional<T> propagate(Iterable<Class<? extends E>> excTypes) throws E {
    for (Class<? extends E> excType : excTypes)
      if (excType.isInstance(exception))
        throw excType.cast(exception);
    return this;
  }

  public <E extends Throwable, F extends Throwable> Exceptional<T> propagate(
      Class<E> excType, Function<? super E, ? extends F> translator)
  throws F
  {
    if (excType.isInstance(exception))
      throw translator.apply(excType.cast(exception));
    return this;
  }

  public <E extends Throwable, F extends Throwable> Exceptional<T> propagate(
      Iterable<Class<E>> excTypes, Function<? super E, ? extends F> translator)
  throws F
  {
    for (Class<? extends E> excType : excTypes)
      if (excType.isInstance(exception))
        throw translator.apply(excType.cast(exception));
    return this;
  }

  public <E extends Throwable> Exceptional<T> handle(Class<E> excType, Consumer<? super E> action) {
    if (excType.isInstance(exception)) {
      action.accept(excType.cast(exception));
      return empty();
    }
    return this;
  }

  public <E extends Throwable> Exceptional<T> handle(Iterable<Class<E>> excTypes, Consumer<? super E> action) {
    for (Class<? extends E> excType : excTypes)
      if (excType.isInstance(exception)) {
        action.accept(excType.cast(exception));
        return empty();
      }
    return this;
  }

  public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) return value;
    if (exception != null) sneakyThrow(exception);
    throw exceptionSupplier.get();
  }

  public boolean isPresent() {
    return value != null;
  }

  public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
      consumer.accept(value);
    if (exception != null) sneakyThrow(exception);
  }

  public boolean isException() {
    return exception != null;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    return obj instanceof Exceptional && Objects.equals(value, ((Exceptional)obj).value);
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(value);
  }

  @SuppressWarnings("unchecked")
  private static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
    throw (T) t;
  }
}

@FunctionalInterface
public interface TrySupplier<T> {
  T tryGet() throws Throwable;
}

@FunctionalInterface
public interface TryRunnable {
  void run() throws Throwable;
}

Ответ 2

Что делать, если каждому функциональному интерфейсу, предоставленному java.util.function, разрешено вызывать исключение?

public interface ThrowingSupplier<R, X extends Throwable> {
    public R get() throws X;
}

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

  • Вы можете отказаться от какого-либо значения или действия по умолчанию
  • Или вы можете попробовать выполнить другое действие, которое может вызвать исключение.

Я написал библиотеку, которая переопределяет большинство интерфейсов в java.util.function таким образом. Я даже предоставляю ThrowingStream, которые позволяют использовать эти новые интерфейсы с тем же API, что и обычный Stream.

@FunctionalInterface
public interface ThrowingSupplier<R, X extends Throwable> {
    public R get() throws X;

    default public Supplier<R> fallbackTo(Supplier<? extends R> supplier) {
        ThrowingSupplier<R, Nothing> t = supplier::get;
        return orTry(t)::get;
    }

    default public <Y extends Throwable> ThrowingSupplier<R, Y> orTry(
            ThrowingSupplier<? extends R, ? extends Y> supplier) {
        Objects.requireNonNull(supplier, "supplier");
        return () -> {
            try {
                return get();
            } catch (Throwable x) {
                try {
                    return supplier.get();
                } catch (Throwable y) {
                    y.addSuppressed(x);
                    throw y;
                }
            }
        };
    }
}

(Nothing - это RuntimeException, который никогда не может быть брошен.)


Ваш оригинальный пример станет

ThrowingFunction<String, Integer, NumberFormatException> parse = Integer::parseInt;
Function<String, Optional<Integer>> safeParse = parse.fallbackTo(s -> null)
    .andThen(Optional::ofNullable);
Stream.of(s1, s2)
    .map(safeParse)
    .map(i -> i.orElse(-1))
    .forEach(System.out::println);

Ответ 3

Вот несколько дискуссий, которые я имел ранее в этой теме.

Я сделал интерфейс Result<T> по рассуждениям. A Result<T> является либо успешным со значением типа T, либо неудачей с Исключением. Это подтип Async<T>, как немедленное завершение асинхронного действия, но это не важно здесь.

Чтобы создать результат -

Result.success( value )
Result.failure( exception )
Result.call( callable )

Результат может быть преобразован различными способами - transform, map, then, peek, catch_, finally_ и т.д. Например

Async<Integer> rInt = Result.success( s )
      .map( Integer::parseInt )
      .peek( System.out::println )
      .catch_( NumberFormatException.class, ex->42 ) // default
      .catch_( Exception.class, ex-> { ex.printStacktrace(); throw ex; } )
      .finally_( ()->{...} )

К сожалению, API фокусируется на Async, поэтому некоторые методы возвращают Async. Некоторые из них могут быть переопределены Результатом, чтобы вернуть результат; но некоторые не могут, например. then() (это плоская карта). Однако, если интересно, легко извлечь автономный API результатов, который не имеет никакого отношения к Async.

Ответ 4

Там есть сторонняя библиотека под названием better-java-monads. Он имеет Try монаду, которая предоставляет необходимые функции. Он также имеет функциональные интерфейсы TryMapFunction и TrySupplier для использования монады Try с проверенными исключениями.