Zoey Hewll
Zoey Hewll

Reputation: 5385

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

}

Upvotes: 6

Views: 4854

Answers (3)

Zoey Hewll
Zoey Hewll

Reputation: 5385

Update: this seems not to work at all. I'm keeping it here for now because I've linked to is elsewhere, and because it uses a method provided in other accepted answers, which I would like to continue to investigate.


Using Federico's answer and the answer linked in the comment, I have deduced a solution with the same method signature as the original problem, and I have created a class which encapsulates this functionality for future use.

The Result implementation:

public class Result<E extends Exception, V>
{
  ...
  public static <E extends Exception, V> Result<E, V> of(ThrowingSupplier<V, E> v)
  {
    try
    {
      return value(v.get());
    }
    catch(Exception e)
    {
      Class<E> errType = Reflector.getType();
      if (errType.isInstance(e))
      {
        return error(errType.cast(e));
      }
      else
      {
        throw (RuntimeException) e;
      }
    }
  }
  ...
}

And the Reflector:

import java.lang.reflect.ParameterizedType;

/**
 * This class only exists to provide a generic superclass to {@link Reflector}
 * @param <E> The type for the subclass to inspect
 */
abstract class Reflected<E>
{ }

/**
 * This class provides the ability to obtain information about its generic type parameter.
 * @param <E> The type to inspect
 * @author
 */
@Deprecated
public class Reflector<E> extends Reflected<E>
{
  /**
   * Returns the class corresponding to the type {@code <E>}.
   * @param <E> The type to inspect
   * @return The class corresponding to the type {@code <E>}
   */
  public static <E> Class<E> getType()
  {
    return new Reflector<E>().getParameterType();
  }

  private Reflector() {}
  private Class<E> getParameterType()
  {
    final ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
    return (Class<E>) type.getActualTypeArguments()[0];
  }
}

Upvotes: 1

Holger
Holger

Reputation: 298183

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.

Upvotes: 2

fps
fps

Reputation: 34460

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...

Upvotes: 2

Related Questions