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
; 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 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 stream() { return value == null ? Stream.empty() : Stream.of(value); } public Exceptional 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 Exceptional flatMap(Function super T, Exceptional> mapper) { Objects.requireNonNull(mapper); return value != null ? Objects.requireNonNull(mapper.apply(value)) : empty(); } public Exceptional 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 Exceptional 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 Exceptional recover( Iterable> 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 Exceptional flatRecover( Class extends X> excType, Function super X, Exceptional > mapper) { Objects.requireNonNull(mapper); return excType.isInstance(exception) ? Objects.requireNonNull(mapper.apply(excType.cast(exception))) : this; } public Exceptional flatRecover( Iterable> excTypes, Function super X, Exceptional > 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 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 extends E> excType : excTypes) if (excType.isInstance(exception)) throw excType.cast(exception); return this; } public Exceptional propagate( Class excType, Function super E, ? extends F> translator) throws F { if (excType.isInstance(exception)) throw translator.apply(excType.cast(exception)); return this; } public Exceptional propagate( Iterable> 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 Exceptional handle(Class excType, Consumer super E> action) { if (excType.isInstance(exception)) { action.accept(excType.cast(exception)); return empty(); } return this; } public Exceptional handle(Iterable> 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 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 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.
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 extends R> supplier) { ThrowingSupplier t = supplier::get; return orTry(t)::get; } default public ThrowingSupplier 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 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.