Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Catching a generic exception

Problem

I am writing a Result type in Java, and I have found a need for it to have a method that performs an operation which may fail, and then encapulates the value or exception in a new Result object.

I had hoped this would work:

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

public class Result<E extends Throwable, V>
{
  ...
  public static <E extends Throwable, V> Result<E, V> of(ThrowingSupplier<V, E> v)
  {
    try
    {
      return value(v.get());
    }
    catch(E e)
    {
      return error(e);
    }
  }
  ...
}

But Java cannot catch an exception defined by a type parameter. I have also tried using instanceof, but that also cannot be used for generics. Is there any way I can implement this method?

Definitions

This is my result type before the addition of the of method. It's intended to be similar to both Haskell's Either and rust's Result, while also having a meaningful bind operation:

public class Result<E extends Throwable, V>
{
  private Either<E, V> value;

  private Result(Either<E, V> value)
  {
    this.value = value;
  }

  public <T> T match(Function<? super E, ? extends T> ef, Function<? super V, ? extends T> vf)
  {
    return value.match(ef, vf);
  }

  public void match(Consumer<? super E> ef, Consumer<? super V> vf)
  {
    value.match(ef, vf);
  }

  /**
   * Mirror of haskell's Monadic (>>=)
   */
  public <T> Result<E, T> bind(Function<? super V, Result<? extends E, ? extends T>> f)
  {
    return match(
        (E e) -> cast(error(e)),
        (V v) -> cast(f.apply(v))
    );
  }

  /**
   * Mirror of Haskell's Monadic (>>) or Applicative (*>)
   */
  public <T> Result<E, T> then(Supplier<Result<? extends E, ? extends T>> f)
  {
    return bind((__) -> f.get());
  }

  /**
   * Mirror of haskell's Applicative (<*)
   */
  public Result<E, V> peek(Function<? super V, Result<? extends E, ?>> f)
  {
    return bind(v -> f.apply(v).then(() -> value(v)));
  }

  public <T> Result<E, T> map(Function<? super V, ? extends T> f)
  {
    return match(
        (E e) -> error(e),
        (V v) -> value(f.apply(v))
    );
  }

  public static <E extends Throwable, V> Result<E, V> error(E e)
  {
    return new Result<>(Either.left(e));
  }

  public static <E extends Throwable, V> Result<E, V> value(V v)
  {
    return new Result<>(Either.right(v));
  }

  /**
   * If the result is a value, return it.
   * If it is an exception, throw it.
   *
   * @return the contained value
   * @throws E the contained exception
   */
  public V get() throws E
  {
    boolean has = match(
        e -> false,
        v -> true
    );
    if (has)
    {
      return value.fromRight(null);
    }
    else
    {
      throw value.fromLeft(null);
    }
  }

  /**
   * Upcast the Result's type parameters
   */
  private static <E extends Throwable, V> Result<E, V> cast(Result<? extends E, ? extends V> r)
  {
    return r.match(
        (E e) -> error(e),
        (V v) -> value(v)
    );
  }
}

And the Either type, designed to closely mirror Haskell's Either:

/**
 * A container for a disjunction of two possible types
 * By convention, the Left constructor is used to hold an error value and the Right constructor is used to hold a correct value
 * @param <L> The left alternative type
 * @param <R> The right alternative type
 */
public abstract class Either<L, R>
{
  public abstract <T> T match(Function<? super L, ? extends T> lf, Function<? super R, ? extends T> rf);

  public abstract void match(Consumer<? super L> lf, Consumer<? super R> rf);

  public <A, B> Either<A, B> bimap(Function<? super L, ? extends A> lf, Function<? super R, ? extends B> rf)
  {
    return match(
        (L l) -> left(lf.apply(l)),
        (R r) -> right(rf.apply(r))
    );
  }

  public L fromLeft(L left)
  {
    return match(
        (L l) -> l,
        (R r) -> left
    );
  }

  public R fromRight(R right)
  {
    return match(
        (L l) -> right,
        (R r) -> r
    );
  }

  public static <L, R> Either<L, R> left(L value)
  {
    return new Left<>(value);
  }

  public static <L, R> Either<L, R> right(R value)
  {
    return new Right<>(value);
  }

  private static <L, R> Either<L, R> cast(Either<? extends L, ? extends R> either)
  {
    return either.match(
        (L l) -> left(l),
        (R r) -> right(r)
    );
  }

  static class Left<L, R> extends Either<L, R>
  {
    final L value;

    Left(L value)
    {
      this.value = value;
    }

    @Override
    public <T> T match(Function<? super L, ? extends T> lf, Function<? super R, ? extends T> rf)
    {
      return lf.apply(value);
    }

    @Override
    public void match(Consumer<? super L> lf, Consumer<? super R> rf)
    {
      lf.accept(value);
    }
  }

  static class Right<L, R> extends Either<L, R>
  {
    final R value;

    Right(R value)
    {
      this.value = value;
    }

    @Override
    public <T> T match(Function<? super L, ? extends T> lf, Function<? super R, ? extends T> rf)
    {
      return rf.apply(value);
    }

    @Override
    public void match(Consumer<? super L> lf, Consumer<? super R> rf)
    {
      rf.accept(value);
    }
  }
}

Example Usage

The main use of this is to convert exception-throwing operations into monadic ones. This allows for (checked) exception-throwing methods to be used in streams and other functional contexts, and also allows for pattern matching and binding on the return type.

private static void writeFiles(List<String> filenames, String content)
{
  filenames.stream()
      .map(
          (String s) -> Result.of(
              () -> new FileWriter(s) //Open file for writing
          ).peek(
              (FileWriter f) -> Result.of(
                  () -> f.write(content) //Write file contents
              )
          ).peek(
              (FileWriter f) -> Result.of(
                  () -> f.close()) //Close file
          )
      ).forEach(
          r -> r.match(
              (IOException e) -> System.out.println("exception writing to file: " + e), //Log exception
              (FileWriter f) -> System.out.println("successfully written to file '" + f + "'") //Log success
          )
      );

}
like image 202
Zoey Hewll Avatar asked Feb 03 '18 11:02

Zoey Hewll


2 Answers

Just use the optimistic assumption that the interface fulfills the contract, as ordinary Java code will always do (enforced by the compiler). If someone bypasses this exception-checking, it’s not your responsibility to fix that:

public static <E extends Exception, V> Result<E, V> of(ThrowingSupplier<V, E> v) {
    try {
        return value(v.get());
    }
    catch(RuntimeException|Error x) {
        throw x; // unchecked throwables
    }
    catch(Exception ex) {
        @SuppressWarnings("unchecked") E e = (E)ex;
        return error(e);
    }
}

Note that even the Java programming language agrees that it is okay to proceed with this assumption, e.g.

public static <E extends Exception, V> Result<E, V> of(ThrowingSupplier<V, E> v) throws E {
    try {
        return value(v.get());
    }
    catch(RuntimeException|Error x) {
        throw x; // unchecked throwables
    }
    catch(Exception ex) {
        throw ex; // can only be E
    }
}

is valid Java code, as under normal circumstances, the get method can only throw E or unchecked throwables, so it is valid to rethrow ex here, when throws E has been declared. We only have to circumvent a deficiency of the Java language when we want to construct a Result parameterized with E.

like image 196
Holger Avatar answered Sep 26 '22 14:09

Holger


You need access to the class of the exception and then use some generics in the catch block.

One simple way is to pass the Class<E> class to the Result.of method:

public static <E extends Throwable, V> Result<E, V> of(
        ThrowingSupplier<V, E> v,
        Class<E> errorType) {

    try {
        return value(v.get());
    } catch(Throwable e) {
        if (errorType.isInstance(e)) {
            return error(errorType.cast(e));
        }
        throw new RuntimeException(e); // rethrow as runtime?
    }
}

Usage:

Result.of(() -> new FileWriter(s), IOException.class)

Class.isInstance is the dynamic equivalent of the instanceof static operator, while Class.cast is the same as statically casting: (E) e, except that we don't get a warning from the compiler.


EDIT: You need to think what to do when the catched Throwable is not of the type of the exception you are expecting. I've wrapped it in a RuntimeException and have rethrown it. This allows to keep using a fluent style for your monad, but is not transparent any more, as now any exception is wrapped in an unchecked exception. Maybe you could add a 3rd argument to Result.of to handle this specific case...

like image 37
fps Avatar answered Sep 25 '22 14:09

fps