A better approach to handling exceptions in a functional way
Asked Answered
T

4

61

Exceptions, especially checked ones, can severely interrupt the flow of program logic when the FP idiom is used in Java 8. Here is an arbitrary example:

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

The above code breaks when there's an exception for an unparseable string. But say I just want to replace that with a default value, much like I can with Optional:

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

Of course, this still fails because Optional only handles nulls. I would like something as follows:

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

Note: this is a self-answered question.

Trilby answered 7/7, 2015 at 14:1 Comment(2)
For the orElse part, instead of returning -1, is there a way to just omit the element. So that the resulting stream would have fewer elements if there are exceptional elements.Brandtr
In the provided example, Exceptional enters the picture in the terminal operation, where the stream's contents are already settled. You can write a different expression that involves a flatMap stage, and you can add a .stream() method to Exceptional: Stream<T> stream() { return isPresent() ? Stream.of(value) : Stream.empty(); } Then you can say stream.flatMap(Exceptional::stream)Trilby
T
57

Presented below is the full code of the Exceptional class. It has a quite large API which is a pure extension of the Optional API so it can be a drop-in replacement for it in any existing code—except that it isn't a subtype of the final Optional class. The class can be seen as being in the same relationship with the Try monad as Optional is with the Maybe monad: it draws inspiration from it, but is adapted to the Java idiom (such as actually throwing exceptions, even from non-terminal operations).

These are some key guidelines followed by the class:

  • as opposed to the monadic approach, doesn't ignore Java's exception mechanism;

  • instead it relieves the impedance mismatch between exceptions and higher-order functions;

  • exception handling not statically typesafe (due to sneaky throwing), but always safe at runtime (never swallows an exception except on explicit request).

The class tries to cover all the typical ways to handle an exception:

  • recover with some handling code which provides a substitute value;
  • flatRecover which, analogous to flatMap, allows to return a new Exceptional instance which will be unwrapped and the state of the current instance suitably updated;
  • propagate an exception, throwing it from the Exceptional expression and making the propagate call declare this exception type;
  • propagate it after wrapping into another exception (translate it);
  • handle it, resulting in an empty Exceptional;
  • as a special case of handling, swallow it with an empty handler block.

The propagate approach allows one to selectively pick which checked exceptions he wants to expose from his code. Exceptions which remain unhandled at the time a terminal operation is called (like get) will be sneakily thrown without declaration. This is often considered as an advanced and dangerous approach, but is nevertheless often employed as a way to somewhat alleviate the nuisance of checked exceptions in combination with lambda shapes which do not declare them. The Exceptional class hopes to offer a cleaner and more selective alternative to sneaky throw.


/*
 * 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;
}
Trilby answered 7/7, 2015 at 14:1 Comment(19)
Don't forget to update your answer if you decide to put this class in some repository :)Indehiscent
@the8472 Stackoverflow => Creative CommonsLunsford
Nice one! Would it make sense to have a handle(Consumer<? super E> action, Class<E>... excType) to allow handle(swallow(), Exception1.class, Exception2.class) (although the order of arguments is less intuitive)...Lunsford
Another example of a 'Try' monad for java by Mario Fusco can be found here: github.com/mariofusco/javaz/blob/master/src/main/java/org/javaz/….Adonic
@Lunsford I thought long about it, but decided it's not worth the loss of intuitiveness, given the availability of asList(Exception1.class, Exception2.class).Trilby
@Adonic Note that Exceptional is not a Try monad, just like Optional is not a Maybe monad. It takes some pragmatic, non-pure FP choices.Trilby
Note that Java 8’s improved type inference even makes the sneaky throw hack more pleasant ;^). I.e. you don’t need the type witness, but can just write Exceptional.sneakyThrow0(t); or, even simpler, sneakyThrow0(t); and the compiler will infer an unchecked exception for you…Equal
@Equal What a nice addition to this subversion of the type system :) But I must say I'm surprised: by what rules does the compiler infer an unchecked exception? I pass a Throwable to sneakyThrow0, which then throws it. Does it infer an unchecked exception just because the method doesn't declare, using the liberties of the unchecked cast?Trilby
@MarkoTopolnik When the compiler is trying to infer the type of X extends Throwable it will prefer an unchecked exception: it directs resolution to optimize the instantiation of α so that, if possible, it is not a checked exception typeMasuria
There are proposals to add a similar type to C++, under the name expected. As in expected<exception, int> is expected to be an int, but if not the reason is exception. See open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4015.pdf -- it is similar, but less tied to exceptions.Unerring
@Equal Actually, the whole inner method becomes redundant: @SuppressWarnings("unchecked") static <T extends Throwable> void sneakyThrow(Throwable t) throws T { throw (T) t; } Now, if that isn't weird code...Trilby
@Yakk I tried to read that spec doc, but it isn't written very clearly. From your description it looks more like the Either monad than the class presented here, which is decidedly not a monad.Trilby
@MarkoTopolnik sort of. It is an extension of optional/Maybe pseudo-monad like yours is, it just replaces the nullopt possibility with the exception (the excuse for it not being the thing it is expected to be). Either monad is not, to my knowledge, biased towards one of the two options. And yes, it is hard to read, as it is written presuming you are fluent in other C++1z proposals, and rather than showing the design is mainly about discussing the design decisions. It is probably less tied to exceptions than your design is.Unerring
@yakk It's actually more like the Try monad (which is left-biased), but I wanted to point out what I see as the main distinction between my class and the monads: expected encapsulates failure as a value whereas my class just allows a nicer idiom to imperatively handle exceptions. Just like Optional is a tool to replace the if statement with an expression (and avoid some boilerplate null-checking), so is Exceptional a tool to do the same with the try-catch statement.Trilby
I don't see the big difference: your map has a catch that returns an exception encapsulated? Once the exception has been encapsulated, you just have methods that let you get the exception out again, or the value, that are chainable?Unerring
In a belated response to Yakk... the main thrust of my design is in the propagate method, which actually throws the exception. A further choice was to always throw any remaining exception upon a terminal operation. This is in stark opposition to the monadic approach, whereas the C++ proposal is in line with it.Trilby
I would like to add one more comment about the monadic nature of Exceptional: yes, it can be shown to satisfy the three monadic laws, just like any other class which happens to define flatMap and has a suitable constructor. If we extended ArrayList with that method, it would "become a monad", too. But there's much more to monads than the formality: it's about how they are actually used and composed with the rest of the system. This is where the intention behind Exceptional starkly diverges from the monadic philosophy, as explained earlier.Trilby
This would benefit from a stream method so you can flatMap a single result (or no result): public Stream<T> stream() { return value == null ? Stream.empty() : Stream.of(value); }Windburn
@Windburn Method added.Trilby
M
10

What if every functional interface provided by java.util.function was allowed to throw an exception?

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

We could use some default methods to provide the behavior you want.

  • 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);
Masuria answered 7/7, 2015 at 16:0 Comment(0)
D
6

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.

Defy answered 7/7, 2015 at 20:18 Comment(22)
This seems pretty similar to CompletableFuture.Masuria
@Masuria - You are right on target. I was hoping that CompletionStage<T> would be the interface we desperately needed for async programming in Java. Such an interface should be a standard one from JDK: nobody wants to create their proprietary ones. Unfortunately, CompletionStage is not suitable to play that role. I was forced to (I did not want to!) create my own for my async http lib.Defy
This looks very well aligned with my goals. One thing that would definitely not work, though, is finally_() because any previous step may have already thrown an exception. To make this work the whole implementation would have to be lazy, just remembering the steps you asked for and then replaying them on a terminal operation. I'm also quite sure that sneaky throwing will never find its way into an API like this one :)Trilby
@MarkoTopolnik - finally_() works, because exceptions in previous lambdas won't leak; map(v->{ throw ex; }) simply transforms the success result to a failure result.Defy
@MarkoTopolnik - while async transformations must be lazy by nature, a synchronous only util like Result can be eager; every transformation is done immediately when the method is called.Defy
Ah, OK. Then it may not be the same idea as mine, which is to actually throw the exceptions (more precisely, to have both choices in the API).Trilby
@MarkoTopolnik - it's an abstraction for "return T or throw Exception"; it can be de-abstracted by getOrThrow()Defy
One of my key ideas is the selective declaration of checked exceptions---so I need more than a blanket getOrThrow. I need to throw with a type filter applied.Trilby
@MarkoTopolnik - that is quite unconventional, mixing two different programming styles. can you provide a use case for propagate() in the middle of transformation chain?Defy
My main use case here is using Exceptional internally inside a method, or inside a lambda body. You call a method, get a bunch of checked exceptions. You want to propagate an IOException as a checked one, but a bunch of reflection exceptions you just want to treat as failures.Trilby
@MarkoTopolnik - that can be done at the last step of the chain, right? not necessary in the middle of the chain.Defy
But I don't want to turn my expression into a try statement to do it. This is much more convenient. If Java didn't have the statement/expression dichotomy, this would probably not be as big a deal.Trilby
@MarkoTopolnik - yes, but I mean I can add a method for getOrThrow(IOException.class) for your requirement, right? But it has to be a terminal method in my API.Defy
I see... but I don't think it's as general. I may want to recover from an exception, then map to something else, getting a new failure, and only the latter case I want to propagate. Exceptional can flip-flop from success to failure and back any number of times.Trilby
@MarkoTopolnik - to simulate short-circuit return/throw, e.g. if(condition) return/throw ... ; rest-of-code, we can use flatmap, e.g. .flatmap( (v,ex)->{ if(condition) return Success/Failure; rest-of-code } )Defy
@bayou.io Out of curiosity, why do you feel that CompletionStage isn't suitable? What would make it more suitable?Masuria
@MarkoTopolnik - rest-of-code is now inside flatmap, in a deeper nesting, instead of as a follow-up clause after flatmap. This is indeed ugly. Maybe the API needs to add short-circuit methods, e.g. shortcut(condition, result) - if condition is met, everything after the shortcut call are ignored.Defy
@Masuria - I got the feeling, from Doug Lea's discussions on the concurrency-interest mailing list, that he is more interested in provide a robust functionality suite, that others can build on top to provide prettier APIs; he himself is not interested in providing a pretty API. I think he knows all those various choices of pretty APIs, and he doesn't want to get involved.Defy
@Masuria - more specifically, what I don't like about it - cancellation mechanism; mutability(!); no checked exceptions in lambda body; bloated APIs, long class/method names; too many methods instead of fewer composable methods; lack of some convenience methods.Defy
more thinking on short-circuit: a better analogy is break statement, which can skip the rest of the code in a block. if we are honest, break is just goto, but forward only. so we could design an API like gotoIf("lable", condition, result).map(...)...transform(...).label("label"). If condition holds, actions between gotoIf and label are skipped. It is very important though to set a matching label, otherwise we cannot transfer this Async to someone else who wants more transformations.Defy
hmm... not much better, visually, than nesting it in flatmap. forget it.Defy
From my current understanding, you either have short circuiting or finally. I'm happier to lose finally, but I think I'm considering a quite different use case than the async completion stage, where it may be more important.Trilby
F
5

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.

Fishbolt answered 7/7, 2015 at 17:43 Comment(1)
I actually designed my class as a response to what I saw in Either and Try. They just regurgitate the same well-known theme of monads which I don't find particularly practical outside Haskell. They strictly adhere to the religion of pure FP, statelessness and immutability. Almost everything is expressed as a type transformation, which suits a powerful type system like Haskell's much better than Java. Also, they don't mesh well with the rest of the standard library and existing code; for example by completely sidestepping the exception system.Trilby

© 2022 - 2024 — McMap. All rights reserved.