Un mejor enfoque para manejar excepciones de una manera funcional

Las excepciones, especialmente las comprobadas, pueden interrumpir gravemente el flujo de la lógica del progtwig cuando se utiliza el modificador 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 no dispersable. 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 aún falla porque Optional solo maneja null s. Me gustaría algo de la siguiente manera:

 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.

A continuación se presenta el código completo de la clase Exceptional . Tiene una API bastante grande que es una extensión pura de la API Optional por lo que puede ser un reemplazo directo en cualquier código existente, excepto que no es un subtipo de la clase Optional final. Se puede ver que la clase está en la misma relación con la mónada Try que con la mónada Maybe : se inspira en ella, pero está adaptada a la expresión de Java (como lanzar excepciones, incluso desde operaciones no 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 su lugar, alivia el desajuste de impedancia entre las excepciones y las funciones de orden superior;

  • el manejo de excepciones no es estático de forma estática (debido a lanzamientos disimulados), pero siempre es seguro en el tiempo de ejecución (nunca se traga una excepción excepto a pedido explícito).

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

  • recover con algún código de manejo que proporcione un valor sustituto;
  • flatRecover que, de forma análoga a flatMap , permite devolver una nueva instancia Exceptional que se flatMap y el estado de la instancia actual se actualizará adecuadamente;
  • propagate una excepción, arrojarla de la expresión Exceptional y hacer que la llamada de propagate declare este tipo de excepción;
  • propagate luego de envolverlo en otra excepción ( traducirlo );
  • handle , lo que resulta en un vacío Exceptional ;
  • como un caso especial de manejo, swallow con un bloque de controlador vacío.

El enfoque de propagate permite a uno seleccionar selectivamente las excepciones marcadas que quiere exponer de su código. Las excepciones que permanecen sin ser manejadas en el momento en que se llama a una operación de terminal (como get ) serán arrojadas furtivamente sin statement. Esto a menudo se considera un enfoque avanzado y peligroso, pero a menudo se emplea como una forma de aliviar un tanto la molestia de las excepciones comprobadas en combinación con las formas lambda que no las declaran. La clase Exceptional espera ofrecer una alternativa más limpia y más 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 { private final T value; private final Throwable exception; private Exceptional(T value, Throwable exc) { this.value = value; this.exception = exc; } public static  Exceptional empty() { return new Exceptional<>(null, null); } public static  Exceptional ofNullable(T value) { return value != null ? of(value) : empty(); } public static  Exceptional of(T value) { return new Exceptional<>(Objects.requireNonNull(value), null); } public static  Exceptional ofNullableException(Throwable exception) { return exception != null? new Exceptional<>(null, exception) : empty(); } public static  Exceptional ofException(Throwable exception) { return new Exceptional<>(null, Objects.requireNonNull(exception)); } public static  Exceptional from(TrySupplier supplier) { try { return ofNullable(supplier.tryGet()); } catch (Throwable t) { return new Exceptional<>(null, t); } } public static Exceptional fromVoid(TryRunnable task) { try { task.run(); return new Exceptional<>(null, null); } catch (Throwable t) { return new Exceptional<>(null, t); } } public static  Consumer 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 other) { if (value != null) return value; if (exception != null) sneakyThrow(exception); return other.get(); } public Stream stream() { return value == null ? Stream.empty() : Stream.of(value); } public Exceptional map(Function 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 Exceptional flatMap(Function> mapper) { Objects.requireNonNull(mapper); return value != null ? Objects.requireNonNull(mapper.apply(value)) : empty(); } public Exceptional filter(Predicate 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  Exceptional recover( Class excType, Function mapper) { Objects.requireNonNull(mapper); return excType.isInstance(exception) ? ofNullable(mapper.apply(excType.cast(exception))) : this; } public  Exceptional recover( Iterable> excTypes, Function mapper) { Objects.requireNonNull(mapper); for (Class excType : excTypes) if (excType.isInstance(exception)) return ofNullable(mapper.apply(excType.cast(exception))); return this; } public  Exceptional flatRecover( Class excType, Function> mapper) { Objects.requireNonNull(mapper); return excType.isInstance(exception) ? Objects.requireNonNull(mapper.apply(excType.cast(exception))) : this; } public  Exceptional flatRecover( Iterable> excTypes, Function> mapper) { Objects.requireNonNull(mapper); for (Class c : excTypes) if (c.isInstance(exception)) return Objects.requireNonNull(mapper.apply(c.cast(exception))); return this; } public  Exceptional propagate(Class excType) throws E { if (excType.isInstance(exception)) throw excType.cast(exception); return this; } public  Exceptional propagate(Iterable> excTypes) throws E { for (Class excType : excTypes) if (excType.isInstance(exception)) throw excType.cast(exception); return this; } public  Exceptional propagate( Class excType, Function translator) throws F { if (excType.isInstance(exception)) throw translator.apply(excType.cast(exception)); return this; } public  Exceptional propagate( Iterable> excTypes, Function translator) throws F { for (Class excType : excTypes) if (excType.isInstance(exception)) throw translator.apply(excType.cast(exception)); return this; } public  Exceptional handle(Class excType, Consumer action) { if (excType.isInstance(exception)) { action.accept(excType.cast(exception)); return empty(); } return this; } public  Exceptional handle(Iterable> excTypes, Consumer action) { for (Class excType : excTypes) if (excType.isInstance(exception)) { action.accept(excType.cast(exception)); return empty(); } return this; } public  T orElseThrow(Supplier 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 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  void sneakyThrow(Throwable t) throws T { throw (T) t; } } 

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

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

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

 public interface ThrowingSupplier { public R get() throws X; } 

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

  • Podría retroceder a algún valor predeterminado o acción
  • O podría intentar realizar otra acción que puede arrojar una excepción

Escribí una biblioteca que redefine la mayoría de las interfaces en java.util.function esta manera. Incluso proporciono un ThrowingStream que te permite usar estas nuevas interfaces con la misma API que un Stream normal.

 @FunctionalInterface public interface ThrowingSupplier { public R get() throws X; default public Supplier fallbackTo(Supplier supplier) { ThrowingSupplier t = supplier::get; return orTry(t)::get; } default public  ThrowingSupplier orTry( ThrowingSupplier 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 una RuntimeException que nunca se puede lanzar).


Su ejemplo original se convertiría

 ThrowingFunction parse = Integer::parseInt; Function> 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 algunas discusiones que tuve previamente sobre este tema.

Hice una interfaz Result largo de los razonamientos. Un Result es un éxito con un valor de tipo T o un error con una excepción. Es un subtipo de Async , como una acción asíncrona completa inmediatamente, pero eso no es importante aquí.

Para crear un resultado –

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

El resultado puede transformarse de varias formas: transform, map, then, peek, catch_, finally_ etc. Por ejemplo

 Async 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 está centrando en Async, por lo que algunos métodos devuelven Async. Algunos de ellos pueden ser anulados por el resultado para devolver el resultado; pero algunos no pueden, por ejemplo, then() (que es flatmap). Sin embargo, si está interesado, es fácil extraer una API de resultados independiente que no tiene nada que ver con Async.

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