Try monad in Java 8

2019-03-18 12:16发布

问题:

Is there a built-in support for monad that deals with exception handling? Something similar to Scala's Try. I am asking because I don't like unchecked exceptions.

回答1:

The "better-java-monads" project on GitHub has a Try monad for Java 8 here.



回答2:

There are at least two generally available (e.g. on Maven Central) - Vavr and Cyclops both have Try implementations that take a slightly differing approach.

Vavr's Try follows Scala's Try very closely. It will catch all 'non-fatal' exceptions thrown during the execution of it's combinators.

Cyclops Try will only catch explicitly configured exceptions (of course you can, by default, also have it catch everything), and the default mode of operating is only to catch during the initial population method. The reasoning behind this is so that Try behaves in a somewhat similar way to Optional - Optional doesn't encapsulate unexpected Null values (i.e. bugs), just places where we reasonable expect to have no value.

Here is an example Try With Resources from Cyclops

 Try t2 = Try.catchExceptions(FileNotFoundException.class,IOException.class)
               .init(()->PowerTuples.tuple(new BufferedReader(new FileReader("file.txt")),new FileReader("hello")))
               .tryWithResources(this::read2);

And another example 'lifting' an existing method (that might divide by zero) to support error handling.

    import static org.hamcrest.Matchers.equalTo;
    import static org.junit.Assert.*;
    import static com.aol.cyclops.lambda.api.AsAnyM.anyM;
    import lombok.val;

    val divide = Monads.liftM2(this::divide);

    AnyM<Integer> result = divide.apply(anyM(Try.of(2, ArithmeticException.class)), anyM(Try.of(0)));

    assertThat(result.<Try<Integer,ArithmeticException>>unwrapMonad().isFailure(),equalTo(true));
 private Integer divide(Integer a, Integer b){
    return a/b;
 }


回答3:

Firstly, let me apologise for answering instead of commenting - apparently I need 50 reputation to comment ...

@ncaralicea your implementation is similar to my own but the problem I had was how to reconcile the try ... catch in bind() with the identity laws. Specifically return x >>= f is equivalent to f x. When bind() catches the exception then f x differs because it throws.

Moreover the ITransformer appears to be a -> b instead of a -> M b. My current version of bind(), unsatisfactory though I find it, is

public <R> MException<R> bind(final Function<T, MException<R>> f) {
    Validate.notNull(f);
    if (value.isRight())
        try {
            return f.apply(value.right().get());
        } catch (final Exception ex) {
            return new MException<>(Either.<Exception, R>left(ex));
        }
    else
        return new MException<>(Either.<Exception, R>left(value.left().get()));
}

where value is an

Either<? extends Exception,T>

The problem with the identity law is that it requires the function f to catch exceptions which defeats the whole purpose of the exercise.

What I think you might actually want is the Functor and not the Monad. That is the fmap : (a->b) -> f a -> f b function.

If you write

@Override
public <R> MException<R> fmap(final Function<T, R> fn) {
    Validate.notNull(fn);
    if (value.isRight())
        try {
            return new MException<>(Either.<Exception, R>right(fn.apply(value.right().get())));
        } catch (final Exception ex) {
            return new MException<>(Either.<Exception, R>left(ex));
        }
    else
        return new MException<>(Either.<Exception, R>left(value.left().get()));
}

then you don't need to write explicit exception handling code, implement new interfaces or mess with the Monad laws.



回答4:

You can do what you want by (ab)using CompletableFuture. Please don't do this in any sort of production code.

CompletableFuture<Scanner> sc = CompletableFuture.completedFuture(
                                                      new Scanner(System.in));

CompletableFuture<Integer> divident = sc.thenApply(Scanner::nextInt);
CompletableFuture<Integer> divisor = sc.thenApply(Scanner::nextInt);

CompletableFuture<Integer> result = divident.thenCombine(divisor, (a,b) -> a/b);

result.whenComplete((val, ex) -> {
    if (ex == null) {
        System.out.printf("%s/%s = %s%n", divident.join(), divisor.join(), val);
    } else {
        System.out.println("Something went wrong");
    }
});


回答5:

Here there is an implementation that could be used as a model. Further information can be found here:

Java with Try, Failure, and Success based computations

You can basically do something like this:

public class Test {

  public static void main(String[] args) {

    ITransformer < String > t0 = new ITransformer < String > () {@
      Override
      public String transform(String t) {
        //return t + t;
        throw new RuntimeException("some exception 1");
      }
    };

    ITransformer < String > t1 = new ITransformer < String > () {@
      Override
      public String transform(String t) {
        return "<" + t + ">";
        //throw new RuntimeException("some exception 2");
      }
    };

    ComputationlResult < String > res = ComputationalTry.initComputation("1").bind(t0).bind(t1).getResult();

    System.out.println(res);

    if (res.isSuccess()) {
      System.out.println(res.getResult());
    } else {
      System.out.println(res.getError());
    }
  }
}

And here is the code:

public class ComputationalTry < T > {

  final private ComputationlResult < T > result;

  static public < P > ComputationalTry < P > initComputation(P argument) {
    return new ComputationalTry < P > (argument);
  }

  private ComputationalTry(T param) {
    this.result = new ComputationalSuccess < T > (param);
  }

  private ComputationalTry(ComputationlResult < T > result) {
    this.result = result;
  }

  private ComputationlResult < T > applyTransformer(T t, ITransformer < T > transformer) {
    try {
      return new ComputationalSuccess < T > (transformer.transform(t));
    } catch (Exception throwable) {
      return new ComputationalFailure < T, Exception > (throwable);
    }
  }

  public ComputationalTry < T > bind(ITransformer < T > transformer) {
    if (result.isSuccess()) {
      ComputationlResult < T > resultAfterTransf = this.applyTransformer(result.getResult(), transformer);
      return new ComputationalTry < T > (resultAfterTransf);
    } else {
      return new ComputationalTry < T > (result);
    }
  }

  public ComputationlResult < T > getResult() {
    return this.result;
  }
}


public class ComputationalFailure < T, E extends Throwable > implements ComputationlResult < T > {

  public ComputationalFailure(E exception) {
    this.exception = exception;
  }

  final private E exception;

  @Override
  public T getResult() {
    return null;
  }

  @Override
  public E getError() {
    return exception;
  }

  @Override
  public boolean isSuccess() {
    return false;
  }

}


public class ComputationalSuccess < T > implements ComputationlResult < T > {

  public ComputationalSuccess(T result) {
    this.result = result;
  }

  final private T result;

  @Override
  public T getResult() {
    return result;
  }

  @Override
  public Throwable getError() {
    return null;
  }

  @Override
  public boolean isSuccess() {
    return true;
  }
}


public interface ComputationlResult < T > {

  T getResult();

  < E extends Throwable > E getError();

  boolean isSuccess();

}


public interface ITransformer < T > {

  public T transform(T t);

}


public class Test {

  public static void main(String[] args) {

    ITransformer < String > t0 = new ITransformer < String > () {@
      Override
      public String transform(String t) {
        //return t + t;
        throw new RuntimeException("some exception 1");
      }
    };

    ITransformer < String > t1 = new ITransformer < String > () {@
      Override
      public String transform(String t) {
        return "<" + t + ">";
        //throw new RuntimeException("some exception 2");
      }
    };

    ComputationlResult < String > res = ComputationalTry.initComputation("1").bind(t0).bind(t1).getResult();

    System.out.println(res);

    if (res.isSuccess()) {
      System.out.println(res.getResult());
    } else {
      System.out.println(res.getError());
    }
  }
}

I hope this might shade some light.



回答6:

@Misha is onto something. Obviously you wouldn't do this exact thing in real code, but CompletableFuture provides Haskell-style monads like this:

  • return maps to CompletableFuture.completedFuture
  • >= maps to thenCompose

So you could rewrite @Misha's example like this:

CompletableFuture.completedFuture(new Scanner(System.in)).thenCompose(scanner ->
CompletableFuture.completedFuture(scanner.nextInt()).thenCompose(divident ->
CompletableFuture.completedFuture(scanner.nextInt()).thenCompose(divisor ->
CompletableFuture.completedFuture(divident / divisor).thenCompose(val -> {
   System.out.printf("%s/%s = %s%n", divident, divisor, val);
   return null;
}))));

which maps to the Haskell-ish:

(return (newScanner SystemIn)) >>= \scanner ->
(return (nextInt scanner)) >>= \divident ->
(return (nextInt scanner)) >>= \divisor ->
(return (divident / divisor)) >>= \val -> do
   SystemOutPrintf "%s/%s = %s%n" divident divisor val
   return Null

or with do syntax

do
   scanner <- return (newScanner SystemIn)
   divident <- return (nextInt scanner)
   divisor <- return (nextInt scanner)
   val <- return (divident / divisor)
   do
       SystemOutPrintf "%s/%s = %s%n" divident divisor val
       return Null

Implementations of fmap and join

I got a little carried away. These are the standard fmap and join implemented in terms of CompletableFuture:

<T, U> CompletableFuture<U> fmap(Function<T, U> f, CompletableFuture<T> m) {
   return m.thenCompose(x -> CompletableFuture.completedFuture(f.apply(x)));
}

<T> CompletableFuture<T> join(CompletableFuture<CompletableFuture<T>> n) {
   return n.thenCompose(x -> x);
}


标签: java-8 monads