development

기능적인 방식으로 예외를 처리하는 더 나은 접근 방식

big-blog 2020. 12. 25. 22:45
반응형

기능적인 방식으로 예외를 처리하는 더 나은 접근 방식


예외, 특히 확인 된 예외는 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)));

물론 s Optional만 처리 하기 때문에 여전히 실패합니다 null. 다음과 같이 싶습니다.

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

참고 : 이것은 스스로 대답하는 질문입니다.


아래에 Exceptional클래스 의 전체 코드가 있습니다. API의 순수한 확장 인 상당히 큰 API가 Optional있으므로 최종 Optional클래스 의 하위 유형이 아니라는 점을 제외하면 기존 코드에서이를 대체 할 수 있습니다 . 클래스는와 같은 관계에있는 것으로 볼 수있는 Try바와 같이 모나드 Optional함께 Maybe그것에서 영감을 그리는 있지만 (심지어 비 단말기 동작에서, 예컨대 실제로 던지는 예외와 같은) 자바 관용구하도록 구성된다 : 모나드.

다음은 수업에서 따르는 몇 가지 주요 지침입니다.

  • 모나 딕 접근 방식과는 반대로 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;
}

에서 제공하는 모든 기능 인터페이스 java.util.function에서 예외를 throw 할 수 있다면 어떨까요?

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

원하는 동작을 제공하기 위해 몇 가지 기본 방법을 사용할 수 있습니다.

  • You could fallback to some default value or action
  • Or you could try to perform another action which may throw an exception

I've written a library which redefines most of the interfaces in java.util.function this way. I even provide a ThrowingStream which let's you use these new interfaces with the same API as a regular 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 is a RuntimeException that can never be thrown.)


Your original example would become

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);

Here's some discussions I had previously on this topic.

I made an interface Result<T> along the reasonings. A Result<T> is either a success with a value of type T, or a failure with an Exception. It's a subtype of Async<T>, as an immediately completed async action, but that is not important here.

To create a result -

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

Result can then be transformed in various ways - transform, map, then, peek, catch_, finally_ etc. For example

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_( ()->{...} )

Unfortunately the API is focusing on Async, so some methods return Async. Some of them can be overridden by Result to return Result; but some cannot, e.g. then() (which is flatmap). However, if interested, it's easy to extract a standalone Result API that has nothing to do with Async.


There's a third-party library called better-java-monads. It has the Try monad which provides the necessary functions. It also has TryMapFunction and TrySupplier functional interfaces to use the Try monad with checked exceptions.

ReferenceURL : https://stackoverflow.com/questions/31270759/a-better-approach-to-handling-exceptions-in-a-functional-way

반응형