Un mejor enfoque para manejar las excepciones de una manera funcional

13 minutos de lectura

avatar de usuario
Marko Topolnik

Las excepciones, especialmente las marcadas, pueden interrumpir severamente el flujo de la lógica del programa cuando se usa el lenguaje FP en Java 8. Aquí hay un ejemplo arbitrario:

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

El código anterior se rompe cuando hay una excepción para una cadena que no se puede analizar. Pero digamos que solo quiero reemplazar eso con un valor predeterminado, al igual que puedo con Optional:

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

Por supuesto, esto todavía falla porque Optional solo manijas nulls. Quisiera algo de lo siguiente:

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

Nota: esta es una pregunta auto-respondida.

  • Para el orElse parte, en lugar de volver -1, ¿hay alguna forma de omitir el elemento? De modo que el flujo resultante tendría menos elementos si hay elementos excepcionales.

    – hrzafer

    22 de febrero de 2018 a las 22:52

  • En el ejemplo proporcionado, Exceptional entra en escena en la operación de terminal, donde el contenido de la transmisión ya está establecido. Puedes escribir una expresión diferente que involucre un flatMap escenario, y puede agregar un .stream() método a Excepcional: Stream<T> stream() { return isPresent() ? Stream.of(value) : Stream.empty(); } Entonces puedes decir stream.flatMap(Exceptional::stream)

    – Marko Topolnik

    23 de febrero de 2018 a las 7:27


avatar de usuario
Marko Topolnik

A continuación se presenta el código completo del Exceptional clase. Tiene una API bastante grande que es una extensión pura de la Optional API por lo que puede ser un reemplazo directo para él en cualquier código existente, excepto que no es un subtipo de la final Optional clase. Se puede considerar que la clase está en la misma relación con el Try mónada como Optional esta con el Maybe mónada: se inspira en él, pero está adaptado al idioma de Java (como lanzar excepciones, incluso desde operaciones que no son terminales).

Estas son algunas pautas clave seguidas por la clase:

  • a diferencia del enfoque monádico, no ignora el mecanismo de excepción de Java;

  • en cambio, alivia el desajuste de impedancia entre las excepciones y las funciones de orden superior;

  • el manejo de excepciones no es seguro para tipos de forma estática (debido al lanzamiento furtivo), pero siempre es seguro en tiempo de ejecución (nunca se traga una excepción excepto por solicitud explícita).

La clase trata de cubrir todas las formas típicas de manejar una excepción:

  • recover con algún código de manejo que proporciona un valor de sustitución;
  • flatRecover que, análogamente a flatMappermite devolver una nueva Exceptional instancia que se desenvolverá y el estado de la instancia actual se actualizará adecuadamente;
  • propagate una excepción, tirándolo desde el Exceptional expresión y hacer la propagate llame a declarar este tipo de excepción;
  • propagate después de envolver en otra excepción (traducir eso);
  • handle ella, resultando en un vacío Exceptional;
  • como un caso especial de manejo, swallow con un bloque de controlador vacío.

los propagate El enfoque permite elegir selectivamente qué excepciones marcadas quiere exponer de su código. Excepciones que permanecen sin manejar en el momento en que se llama a una operación de terminal (como get) estarán a escondidas lanzado sin declaración. Esto a menudo se considera un enfoque avanzado y peligroso, pero, sin embargo, a menudo se emplea como una forma de aliviar un poco la molestia de las excepciones comprobadas en combinación con las formas lambda que no las declaran. los Exceptional La clase espera ofrecer una alternativa más limpia y selectiva al lanzamiento furtivo.


/*
 * 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
    }
    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;
}

  • No olvide actualizar su respuesta si decide poner esta clase en algún repositorio 🙂

    – Pshemó

    7 julio 2015 a las 14:40


  • @the8472 Desbordamiento de pila => comunes creativos

    – asilias

    7 julio 2015 a las 15:11

  • Otro ejemplo de una mónada ‘Try’ para Java de Mario Fusco se puede encontrar aquí: github.com/mariofusco/javaz/blob/master/src/main/java/org/javaz/….

    – Jeff

    7 julio 2015 a las 15:19


  • @MarkoTopolnik Cuando el compilador intenta inferir el tipo de X extends Throwable va a prefiero una excepción no verificada: it directs resolution to optimize the instantiation of α so that, if possible, it is not a checked exception type

    – Jeffrey

    7 julio 2015 a las 16:59

  • Hay propuestas para agregar un tipo similar a C++, bajo el nombre expected. Como en expected<exception, int> se espera que sea un intpero si no la razón es exception. Ver open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4015.pdf — es similar, pero menos ligado a excepciones.

    – Yakk – Adam Nevraumont

    7 julio 2015 a las 17:06

avatar de usuario
jeffrey

¿Qué pasa si cada interfaz funcional proporcionada por java.util.function se le permitió lanzar una excepción?

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

Podríamos usar algunos métodos predeterminados para proporcionar el comportamiento que desea.

  • Tú podrías recurrir a algún valor predeterminado o acción
  • O tú podrías probar para realizar otra acción que puede generar una excepción

He escrito una biblioteca que redefine la mayoría de las interfaces en java.util.function Por aquí. Incluso proporciono un ThrowingStream que le permite usar estas nuevas interfaces con la misma API que una normal 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
    }

    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 es un RuntimeException que nunca se puede tirar.)


Tu ejemplo original se convertiría en

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

Aquí hay algunos discusiones que tenía anteriormente sobre este tema.

hice una interfaz Result<T> a lo largo de los razonamientos. A Result<T> es un éxito con un valor de tipo T, o un error con una excepción. es un subtipo de Async<T>como una acción asíncrona completada inmediatamente, pero eso no es importante aquí.

Para crear un resultado –

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

El resultado se puede transformar de varias maneras: transform, map, then, peek, catch_, finally_ etc. Por ejemplo

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

Lamentablemente, la API se centra en Async, por lo que algunos métodos devuelven Async. Algunos de ellos pueden ser anulados por Result para devolver Result; pero algunos no pueden, por ejemplo then() (que es mapa plano). Sin embargo, si está interesado, es fácil extraer una API de resultados independiente que no tiene nada que ver con Async.

  • Esto se ve muy bien alineado con mis objetivos. Sin embargo, una cosa que definitivamente no funcionaría es finally_() porque es posible que cualquier paso anterior ya haya arrojado una excepción. Para que esto funcione, toda la implementación tendría que ser perezosa, solo recordar los pasos que solicitó y luego reproducirlos en una operación de terminal. También estoy bastante seguro de que el lanzamiento furtivo nunca encontrará su camino en una API como esta 🙂

    – Marko Topolnik

    7 julio 2015 a las 20:48


  • Está bien. Entonces puede que no sea la misma idea que la mía, que en realidad es lanzar las excepciones (más precisamente, tener ambas opciones en la API).

    – Marko Topolnik

    7 julio 2015 a las 20:57


  • Una de mis ideas clave es la declaración selectiva de excepciones verificadas, por lo que necesito más que una manta getOrThrow. Necesito lanzar con un filtro de tipo aplicado.

    – Marko Topolnik

    7 julio 2015 a las 20:59

  • Mi caso de uso principal aquí es usar Exceptional internamente dentro de un método, o dentro de un cuerpo lambda. Llamas a un método, obtienes un montón de excepciones verificadas. Quiere propagar un IOException como uno marcado, pero un montón de excepciones de reflexión que solo desea tratar como fallas.

    – Marko Topolnik

    7 julio 2015 a las 21:12

  • @Jeffrey, más específicamente, lo que no me gusta de él: mecanismo de cancelación; mutabilidad(!); sin excepciones marcadas en el cuerpo lambda; API infladas, nombres largos de clases/métodos; demasiados métodos en lugar de menos métodos componibles; falta de algunos métodos de conveniencia.

    – Zhong Yu

    7 julio 2015 a las 21:51


Hay una biblioteca de terceros llamada mejor-java-monads. tiene el Try mónada que proporciona las funciones necesarias. También tiene TryMapFunction y TrySupplier interfaces funcionales para utilizar el Try mónada con excepciones marcadas.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad