diff --git a/README.md b/README.md index abd9cb0..905e9e8 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,87 @@ [![Java CI with Gradle](https://github.com/xyzsd/dichotomy/actions/workflows/gradle.yml/badge.svg)](https://github.com/xyzsd/dichotomy/actions/workflows/gradle.yml) # dichotomy -Either and Result monadic types for Java. -Includes specialized types Try and Maybe. +Sealed monads for Java. -All types are sealed, and can be used in switch statements and +Generally these types are used to return one of two values, such as success or failure. + +All types are sealed (Sum types), and can be used in `switch` expressions and with pattern matching. -## UPDATE (18-Dec-2023): - * Substantially restructured and improved for the pending 1.0 version - * Markedly improved Try type, added Maybe type - * Tests near-complete - * Improved documentation - * still needs: a nice intro (with synopsis and illustrative examples) +### `Either`: +An general immutable type that can only be *either* one of two types. +The types are called `Left` and `Right`. By convention, the Left type +indicates failure, while the Right type indicates success. + +### `Result`: +Similar to an `Either`, but with success/failure semantics more clearly defined. +An `OK` Result indicates success, and an `Err` Result indicates failure. Failure +types do not need to be Exceptions. + + ```java + +Result result = Result.ofOK(3828) // returns an OK + .map(x -> x*10.0) // map to Result, after multiplying x 10 + .match(System.out::println) // print "38280.0" to console + .matchErr(System.err::println); // ignored, as this is an OK + +switch(result) { + case OK ok -> System.out.println("value ok! value: "+ok.value()); + case Err err -> System.err.println(err.value()); +} + +// JDK 21+ +switch(result) { + case OK(Double x) when x > 0 -> System.out.println("positive"); + case OK(Double x) -> System.out.println("0 or negative"); + case Err(String s) -> System.err.println(s); +} + +// anotherResult here will be an Err +Result anotherResult = Result.ofErr("Insufficient entropy") + .map(x -> x*10.0 ) // ignored, as this is an Err + .match(System.out::println) // ignored, as this is an Err + .matchErr(System.err::println); // "Insufficient entropy" printed to System.err +``` + + +### `Try`: +A specialized type of `Result`. A `Try` wraps a function or block; if +successful, a `Success` Try is returned; otherwise, a `Failure` Try containing +an Exception is returned. Intermediate operations which return Trys will also +catch generated Exceptions. + +```java + +final Try result = Try.ofSuccess( 777 ) + .map( i -> i * 1000 ) // results in a Try with a value of 777000 + .exec( System.out::println ) // prints "777000" + .map( i -> i / 0 ) // the ArithmeticException is caught as a Try.Failure + .exec( System.out::println ); // does not exec() because we are a Failure + +// prints "ERROR: java.lang.ArithmeticException: / by zero" +switch(result) { + case Success(Integer i) -> System.out.printf("Operation completed successfully. Value: %d\n", i); + case Failure(Throwable t) -> System.err.printf("ERROR: %s\n", t); +} + + +``` + +### `Maybe`: +Analogous to the JDK `Optional` type, but sealed so it may be used in `switch` +statements and with pattern matching. + +## Updates (January 2024) + - [x] Refactored, now with substantial improvements + - [x] New Try class + - [x] New Maybe class (analogous to Optional) + - [x] Near-complete test coverage + - [x] Improved documentation + - [x] Targets JDK 21 + - [ ] Examples (still in progress) + - [ ] Maven release Download -------- diff --git a/src/main/java/net/xyzsd/dichotomy/Result.java b/src/main/java/net/xyzsd/dichotomy/Result.java index a29ecf3..2eaecfa 100644 --- a/src/main/java/net/xyzsd/dichotomy/Result.java +++ b/src/main/java/net/xyzsd/dichotomy/Result.java @@ -1182,13 +1182,10 @@ public boolean containsErr(@Nullable E errValue) { @Override public @NotNull V expect() throws RuntimeException { - // TODO: when pattern-switch is out of preview, convert this code - if (value instanceof RuntimeException e) { - throw e; - } else if (value instanceof Throwable t) { - throw new NoSuchElementException( t ); - } else { - throw new NoSuchElementException( String.valueOf( value ) ); + switch(value) { + case RuntimeException e -> throw e; + case Throwable t -> throw new NoSuchElementException(t); + default -> throw new NoSuchElementException( String.valueOf( value ) ); } } @@ -1198,7 +1195,7 @@ public boolean containsErr(@Nullable E errValue) { throw requireNonNull( exFn.apply( value ) ); } - // For types where the value type is unchanged and exists, but the generic type of the value differs + // For types where the value type is unchanged and exists, but the generic type of the value differs, // just cast and return. Types are erased so there is no need to create a new object. // The Error stays the same; only the empty value signature changes @SuppressWarnings("unchecked") diff --git a/src/main/java/net/xyzsd/dichotomy/collectors/EitherCollectors.java b/src/main/java/net/xyzsd/dichotomy/collectors/EitherCollectors.java index e0f6ccd..dc9df2c 100644 --- a/src/main/java/net/xyzsd/dichotomy/collectors/EitherCollectors.java +++ b/src/main/java/net/xyzsd/dichotomy/collectors/EitherCollectors.java @@ -83,13 +83,9 @@ public interface EitherCollectors { static private void add(Accumulator listBox, Either either) { - // TODO: use switch/case when we support JDK > 20 - if(either instanceof Either.Right right) { - listBox.okList.add( right.value() ); - } else if(either instanceof Either.Left left) { - listBox.errList.add( left.value() ); - } else { - throw new IllegalStateException(); + switch(either) { + case Either.Right(R r) -> listBox.okList.add( r ); + case Either.Left(L l) -> listBox.errList.add( l ); } } diff --git a/src/main/java/net/xyzsd/dichotomy/collectors/ResultCollectors.java b/src/main/java/net/xyzsd/dichotomy/collectors/ResultCollectors.java index 4ebd326..7926798 100644 --- a/src/main/java/net/xyzsd/dichotomy/collectors/ResultCollectors.java +++ b/src/main/java/net/xyzsd/dichotomy/collectors/ResultCollectors.java @@ -82,13 +82,9 @@ public interface ResultCollectors { static private void add(Accumulator listBox, Result result) { - // TODO: use switch/case when we support JDK > 20 - if(result instanceof Result.OK ok) { - listBox.okList.add( ok.value() ); - } else if(result instanceof Result.Err err) { - listBox.errList.add( err.value() ); - } else { - throw new IllegalStateException(); + switch(result) { + case Result.OK(OK v) -> listBox.okList.add( v ); + case Result.Err(ERR e) -> listBox.errList.add( e ); } } diff --git a/src/main/java/net/xyzsd/dichotomy/trying/Try.java b/src/main/java/net/xyzsd/dichotomy/trying/Try.java index 0f07e2b..8760948 100644 --- a/src/main/java/net/xyzsd/dichotomy/trying/Try.java +++ b/src/main/java/net/xyzsd/dichotomy/trying/Try.java @@ -2,10 +2,7 @@ import net.xyzsd.dichotomy.Empty; import net.xyzsd.dichotomy.Result; -import net.xyzsd.dichotomy.trying.function.ExBiFunction; -import net.xyzsd.dichotomy.trying.function.ExConsumer; -import net.xyzsd.dichotomy.trying.function.ExFunction; -import net.xyzsd.dichotomy.trying.function.ExSupplier; +import net.xyzsd.dichotomy.trying.function.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,8 +42,6 @@ * If a method which takes an {@link ExConsumer}, {@link ExSupplier}, or {@link ExFunction} returns {@code null}, * the {@code NullPointerException} will be caught and returned as a {@link Failure}. * - * - * * @param the Try type, held by Success values. */ public sealed interface Try permits Try.Failure, Try.Success { @@ -55,8 +50,8 @@ public sealed interface Try permits Try.Failure, Try.Success { * Create a Successful Try. * * @param value contained by the {@link Success} + * @param value type * @return {@link Success} containing the above value - * @param value type */ @NotNull static Try ofSuccess(@NotNull T value) { @@ -67,8 +62,8 @@ static Try ofSuccess(@NotNull T value) { * Create an unsuccessful Try (Failure). * * @param failure contained by the {@link Failure} + * @param Success value type (always empty in this case). * @return {@link Failure} containing the above value - * @param Success value type (always empty in this case). */ @NotNull static Try ofFailure(@NotNull Throwable failure) { @@ -76,33 +71,33 @@ static Try ofFailure(@NotNull Throwable failure) { } - // todo: show an example just wrapping a block of code. + /** * Invoke an {@link ExSupplier} to create a {@link Try}. *

- * If the {@link ExSupplier} is successful, a {@link Success} will be returned. - * If an exception is thrown by the {@link ExSupplier}, a {@link Failure} will be returned. + * If the {@link ExSupplier} is successful, a {@link Success} will be returned. + * If an exception is thrown by the {@link ExSupplier}, a {@link Failure} will be returned. *

*

- * {@link ExSupplier}s can throw checked or unchecked Exceptions. - * To use a {@link Supplier} (which can only throw RuntimeExceptions), use as follows: - * {@snippet : + * {@link ExSupplier}s can throw checked or unchecked Exceptions. + * To use a {@link Supplier} (which can only throw RuntimeExceptions), use as follows: + * {@snippet : * Try suppliedTry = Try.of( myStringSupplier::get ); * // or alternatively: - * Try suppliedTry2 = Try.of( ExSupplier.from(myStringSupplier) ); - * } + * Try suppliedTry2 = Try.wrap( ExSupplier.from(myStringSupplier) ); + *} *

*

- * If the {@code ExSupplier} returns {@code null}, this method - * will return a {@link Failure} containing a {@link NullPointerException}. + * If the {@code ExSupplier} returns {@code null}, this method + * will return a {@link Failure} containing a {@link NullPointerException}. *

* * @param xSupplier a Supplier which could potentially throw an Exception. + * @param non-exceptional type * @return supplied type or exception wrapped in a Try - * @param non-exceptional type */ @NotNull - static Try of(@NotNull ExSupplier xSupplier) { + static Try wrap(@NotNull ExSupplier xSupplier) { requireNonNull( xSupplier ); try { // we still need to wrap in a requireNonNull() because we could create @@ -114,30 +109,72 @@ static Try of(@NotNull ExSupplier xSupplier) { } + /** + * Invoke an {@link ExRunnable} to create a {@link Try}. + *

+ * If the {@link ExRunnable} is successful, a {@link Success} will be returned. + * If an exception is thrown by the {@link ExRunnable}, a {@link Failure} will be returned. + *

+ *

+ * {@link ExRunnable}s can throw checked or unchecked Exceptions. + * To use a {@link Runnable} (which can only throw RuntimeExceptions), use as follows: + * {@snippet : + * Try firstTry = Try.of( myRunnable::run ); + * // or alternatively: + * Try secondTry = Try.wrap( ExRunnable.from(myRunnable) ); + *} + *

+ *

+ * This can also be used to wrap blocks of code that could throw exceptions: + * {@snippet : + * Try blockTry = Try.wrap( () -> { + * String in = Files.readString( Path.of("path-to-a-file") ); + * if(in.startsWith( "begin:")) { + * Files.writeString( Path.of("path-to-another-file") ); + * } + * }); + *} + *

+ * @param runnable a Runnable which could potentially throw an Exception + * @return Empty Try or an exception wrapped in a Try + */ + @NotNull + static Try wrap(@NotNull ExRunnable runnable) { + requireNonNull( runnable ); + try { + runnable.run(); + return Try.Success.of(); + } catch (Throwable t) { + return Failure.of( t ); + } + } + + + /** * Try-with-resources for a single AutoCloseable. *

- * The ExSupplier will supply the AutoCloseable resource. - * The ExFunction will take the supplied resource and return a result. - * The supplied resource will be automatically closed (as per try-with-resource semantics). - * If no exceptions occur, the function result will be wrapped in a {@link Success}. + * The ExSupplier will supply the AutoCloseable resource. + * The ExFunction will take the supplied resource and return a result. + * The supplied resource will be automatically closed (as per try-with-resource semantics). + * If no exceptions occur, the function result will be wrapped in a {@link Success}. *

*

- * A {@link Failure} will be returned, containing an Exception when: - *

    - *
  • An Exception occurs in the supplier (if so, the function will not be invoked)
  • - *
  • An Exception occurs in the function (if so, the resource will still be closed)
  • - *
  • An Exception occurs when the resource is closed
  • - *
- * If an Exception occurs in the function (but not the supplier) and during resource closing, the - * resource-closing Exception will be added as a suppressed exception (unless disallowed) to the - * Exception thrown during function execution. + * A {@link Failure} will be returned, containing an Exception when: + *
    + *
  • An Exception occurs in the supplier (if so, the function will not be invoked)
  • + *
  • An Exception occurs in the function (if so, the resource will still be closed)
  • + *
  • An Exception occurs when the resource is closed
  • + *
+ * If an Exception occurs in the function (but not the supplier) and during resource closing, the + * resource-closing Exception will be added as a suppressed exception (unless disallowed) to the + * Exception thrown during function execution. * * @param supplier AutoCloseable resource supplier - * @param fn ExFunction to process AutoCloseable resource + * @param fn ExFunction to process AutoCloseable resource + * @param ExFunction result + * @param AutoCloseable resource and ExFunction input * @return ExFunction result wrapped in a Try - * @param ExFunction result - * @param AutoCloseable resource and ExFunction input */ static Try withResources(ExSupplier supplier, ExFunction fn) { requireNonNull( supplier ); @@ -154,14 +191,13 @@ static Try withResources(ExSupplier supplie * Two-supplier, Two-parameter try-with-resources. * See {@link #withResources(ExSupplier, ExFunction)}. * - * * @param supplier1 First AutoCloseable resource supplier * @param supplier2 Second AutoCloseable resource supplier - * @param biFn function which can use both AutoCloseable resources supplied + * @param biFn function which can use both AutoCloseable resources supplied + * @param biFunction result + * @param First AutoCloseable resource and ExBiFunction input + * @param Second AutoCloseable resource and ExBiFunction input * @return biFn result - * @param biFunction result - * @param First AutoCloseable resource and ExBiFunction input - * @param Second AutoCloseable resource and ExBiFunction input */ static Try withResources(ExSupplier supplier1, ExSupplier supplier2, @@ -180,12 +216,14 @@ static Try withReso /** * Determines if this is a Failure. + * * @return {@code true} if this is a {@link Failure}. */ boolean isFailure(); /** * Determines if this is a Success. + * * @return {@code true} if this is a {@link Success}. */ boolean isSuccess(); @@ -199,7 +237,7 @@ static Try withReso * * @param fnSuccess the mapping function for {@link Success} values. * @param fnFailure the mapping function for {@link Failure} values. - * @param returned type, which can be different from the original type + * @param returned type, which can be different from the original type * @return the {@link Try} produced from {@code fnSuccess} or {@code fnFailure} * @throws NullPointerException if the called function returns {@code null}. * @see #map(ExFunction) @@ -208,8 +246,6 @@ static Try withReso @NotNull ExFunction> fnFailure); - - /** * Returns a value, produced from one of the appropriate mapping functions. *

@@ -219,7 +255,7 @@ static Try withReso * * @param fnSuccess the mapping function for {@link Failure} values. * @param fnFailure the mapping function for {@link Success} values. - * @param returned type, which must be the same for both Failure and Success values. + * @param returned type, which must be the same for both Failure and Success values. * @return the value produced from {@code fnSuccess} or {@code fnFailure} * @throws NullPointerException if the called function returns {@code null}. * @see #recover(Function) @@ -258,8 +294,8 @@ static Try withReso /** * Executes the action iff this is an {@link Success} {@link Try}. * - * @return {@code this} if successful, otherwise returns a {@link Failure} containing the Exception. * @param successConsumer Consumer of Success values + * @return {@code this} if successful, otherwise returns a {@link Failure} containing the Exception. * @throws NullPointerException if successConsumer is {@code null}. * @see #consume(Consumer) * @see #consumeErr(Consumer) @@ -299,7 +335,7 @@ static Try withReso *

* * @param fnSuccess the mapping function producing a new {@link Success} value. - * @param returned type, which can be different from the original type + * @param returned type, which can be different from the original type * @return a new {@link Success} produced by the mapping function, if applied. */ @NotNull Try map(@NotNull ExFunction fnSuccess); @@ -332,7 +368,7 @@ static Try withReso * * @param fnSuccess the mapping function for {@link Success} values. * @param fnFailure the mapping function for {@link Failure} values. - * @param returned type, which can be different from the original type + * @param returned type, which can be different from the original type * @return the {@link Try} produced from {@code okMapper} or {@code errMapper}, or a {@link Failure} if an Exception is caught. * @see #map(ExFunction) * @see #mapErr(ExFunction) @@ -356,7 +392,7 @@ static Try withReso *

* * @param fnSuccess the mapping function that produces a new {@link Try} - * @param returned type, which can be different from the original type + * @param returned type, which can be different from the original type * @return a new {@link Try} produced by the mapping function, if applied * @see #flatMapErr(ExFunction) * @see #biFlatMap(ExFunction, ExFunction) @@ -432,17 +468,16 @@ static Try withReso @NotNull V orElseGet(@NotNull Supplier okSupplier); - /** * If {@code this} is {@link Failure}, return it. Otherwise, return the next {@link Try} given. * The next {@link Try} can have a different parameterized type. * * @param nextTry The {@link Try} to return. - * @param type of the value, which can be different from the original type + * @param type of the value, which can be different from the original type + * @return this or the given Try * @see #and(ExSupplier) * @see #or(Try) * @see #or(ExSupplier) - * @return this or the given Try */ @NotNull Try and(@NotNull Try nextTry); @@ -453,12 +488,12 @@ static Try withReso * The next {@link Try} can have a different parameterized type. * * @param nextTrySupplier The supplier of a {@link Try} to return; only called if {@code this} is a {@link Success}. - * @param type of the value, which can be different from the original type + * @param type of the value, which can be different from the original type + * @return this or the supplied Try * @throws NullPointerException if the supplied {@link Try} is {@code null}. * @see #and(Try) * @see #or(Try) * @see #or(ExSupplier) - * @return this or the supplied Try */ default @NotNull Try and(@NotNull final ExSupplier> nextTrySupplier) { requireNonNull( nextTrySupplier ); @@ -466,17 +501,15 @@ static Try withReso } - - /** * If {@code this} is {@link Success}, return it. * Otherwise, return the next {@link Try} given. * * @param nextTry The {@link Try} to return. + * @return this or the given Try * @see #or(ExSupplier) * @see #and(Try) * @see #and(ExSupplier) - * @return this or the given Try */ @NotNull Try or(@NotNull Try nextTry); @@ -485,11 +518,11 @@ static Try withReso * Otherwise, return the next {@link Try} supplied. * * @param nextTrySupplier The supplier of a {@link Try} to return; only called if {@code this} is a {@link Failure}. + * @return this or the supplied Try * @throws NullPointerException if the supplier is called and returns {@code null}. * @see #or(Try) * @see #and(Try) * @see #and(ExSupplier) - * @return this or the supplied Try */ default @NotNull Try or(@NotNull ExSupplier> nextTrySupplier) { requireNonNull( nextTrySupplier ); @@ -497,9 +530,6 @@ static Try withReso } - - - /** * Recover from an value; ignore the {@link Failure} value if present, * and apply the mapping function to get an {@link Success}. @@ -559,7 +589,6 @@ static Try withReso * getOrThrow( (x) -> new IOException("This is my exception message")); *} * - * * @param exFn Exception (Throwable) mapping function * @param Exception to throw; if not a (subclass of) RuntimeException, it must be rethrown or caught * @return V @@ -590,7 +619,6 @@ private static Try flatMapChecked(IN in, ExFunction void sneakyThrow(Throwable e) throws E { throw (E) e; @@ -605,11 +633,12 @@ private static void sneakyThrow(Throwable e) throws E { * A Successful Try. * * @param value Successful value - * @param value type parameter + * @param value type parameter */ record Success(@NotNull T value) implements Try { /** * A Successful Try. + * * @param value successful value. */ public Success { @@ -618,6 +647,7 @@ record Success(@NotNull T value) implements Try { /** * Create a Successful empty Try. + * * @return Successful Try with the {@link Empty} type. */ public static Success of() { @@ -626,8 +656,9 @@ public static Success of() { /** * Create a Successful Try. + * * @param value successful value. - * @param type of value + * @param type of value * @return Success containing the given value */ public static Success of(@NotNull final U value) { @@ -775,7 +806,6 @@ public boolean contains(@Nullable T okValue) { } - @Override public @NotNull T recover(@NotNull Function fnFailureToSuccess) { requireNonNull( fnFailureToSuccess ); @@ -813,6 +843,7 @@ record Failure(@NotNull Throwable err) implements Try { /** * An Unsuccessful Try. This will never permit 'fatal' exceptions (see documentation for {@link Try}). + * * @param err Throwable */ public Failure { @@ -822,9 +853,10 @@ record Failure(@NotNull Throwable err) implements Try { /** * Create an Unsuccessful Try. This will never permit 'fatal' exceptions (see documentation for {@link Try}). - * @param t value value - * @return {@code Try.Failure} containing the value value. + * + * @param t value value * @param Success value type (always empty) + * @return {@code Try.Failure} containing the value value. */ public static Failure of(@NotNull Throwable t) { return new Failure<>( t ); @@ -997,11 +1029,11 @@ private Failure coerce() { * For Throwables which are truly fatal, we will not capture. */ private static void throwIfFatal(final Throwable t) { - // TODO: add MatchException when we target JDK > 20 - // and restructure to a switch-case w/pattern if (t instanceof VirtualMachineError || t instanceof LinkageError) { - // Errors are similar to unchecked exceptions + // Errors are similar to unchecked (Runtime) exceptions throw (Error) t; + } else if (t instanceof MatchException me) { + throw me; // a RuntimeException } else if (t instanceof InterruptedException) { sneakyThrow( t ); } diff --git a/src/main/java/net/xyzsd/dichotomy/trying/function/ExRunnable.java b/src/main/java/net/xyzsd/dichotomy/trying/function/ExRunnable.java new file mode 100644 index 0000000..5e5ce64 --- /dev/null +++ b/src/main/java/net/xyzsd/dichotomy/trying/function/ExRunnable.java @@ -0,0 +1,33 @@ +package net.xyzsd.dichotomy.trying.function; + +import org.jetbrains.annotations.NotNull; + + +import static java.util.Objects.requireNonNull; + +/** + * An {@link ExRunnable} is equivalent to a {@link Runnable}, but the + * {@link ExRunnable#run()} method can throw a checked Exception. + * + */ +@FunctionalInterface +public interface ExRunnable { + + /** + * Execute the given operation. + * @throws Throwable Exception + */ + void run() throws Throwable; + + /** + * Convert a Runnable to an ExRunnable. The Consumer is only invoked when used. + * + * @param runnable the Runnable + * @return ExRunnable + */ + @NotNull static ExRunnable from(@NotNull final Runnable runnable) { + requireNonNull( runnable ); + return runnable::run; + } + +} diff --git a/src/main/java/net/xyzsd/dichotomy/trying/function/package-info.java b/src/main/java/net/xyzsd/dichotomy/trying/function/package-info.java index 3053f96..776bdc1 100644 --- a/src/main/java/net/xyzsd/dichotomy/trying/function/package-info.java +++ b/src/main/java/net/xyzsd/dichotomy/trying/function/package-info.java @@ -8,5 +8,10 @@ * These interfaces are primarily provided for use with {@link net.xyzsd.dichotomy.trying.Try}, to * allow functional code to wrap and use exception-generating methods in a convenient manner. *

+ *

+ * These interfaces need be used if Runtime exceptions are thrown; instead, standard JDK + * functional interfaces (e.g., {@code Runnable, Consumer, Supplier, Function, BiFunction}) + * can be used instead. + *

*/ package net.xyzsd.dichotomy.trying.function; \ No newline at end of file diff --git a/src/test/java/net/xyzsd/dichotomy/ResultTest.java b/src/test/java/net/xyzsd/dichotomy/ResultTest.java index 1593b76..5caeb9b 100644 --- a/src/test/java/net/xyzsd/dichotomy/ResultTest.java +++ b/src/test/java/net/xyzsd/dichotomy/ResultTest.java @@ -565,4 +565,45 @@ void monadicLaws() { MTEST_ERR.flatMapErr( s -> hundredMore.apply( s ).flatMapErr( square ) ) ); } + + @Test + void highlyContrivedAndNotVeryGoodExamples() { + // OK: first example + final Result firstResult = Result.ofOK( 3828 ) // returns an OK + .map( x -> x * 10.0 ) // map to float, after multiplying x 10 + .match( System.out::println ) // print "38280.0" to console + .matchErr( System.err::println );// ignored, as this is an OK + + // prints 'value ok!' + switch(firstResult) { + case OK ok -> System.out.println("value ok! value: "+ok.value()); + case Err err -> System.err.println(err.value()); + } + + // prints 'positive' + switch(firstResult) { + case OK(Double x) when x > 0 -> System.out.println("positive"); + case OK(Double x) -> System.out.println("0 or negative"); + case Err(String s) -> System.err.println(s); + } + + // Err: second example + Result errResult = Result.ofErr("Insufficient entropy") + .map(x -> x*10.0 ) // ignored, as this is an Err + .match(System.out::println) // ignored, as this is an Err + .matchErr(System.err::println); // "Insufficient entropy" printed to System.err + + // prints 'ERROR: Insufficient entropy' + switch(errResult) { + case OK ok -> System.out.println("value ok! value: "+ok.value()); + case Err err -> System.err.printf("ERROR: %s\n", err.value()); + } + + // prints 'ERROR: Insufficient entropy' + switch(errResult) { + case OK(Double x) when x > 0 -> System.out.println("positive"); + case OK(Double x) -> System.out.println("0 or negative"); + case Err(String s) -> System.err.printf("ERROR: %s\n", s); + } + } } \ No newline at end of file diff --git a/src/test/java/net/xyzsd/dichotomy/trying/TryTest.java b/src/test/java/net/xyzsd/dichotomy/trying/TryTest.java index f4abd7d..1c7a180 100644 --- a/src/test/java/net/xyzsd/dichotomy/trying/TryTest.java +++ b/src/test/java/net/xyzsd/dichotomy/trying/TryTest.java @@ -1,9 +1,11 @@ package net.xyzsd.dichotomy.trying; -import net.xyzsd.dichotomy.Empty; import net.xyzsd.dichotomy.TestUtils.SingleUseConsumer; +import net.xyzsd.dichotomy.trying.Try.Failure; +import net.xyzsd.dichotomy.trying.Try.Success; import net.xyzsd.dichotomy.trying.function.ExConsumer; import net.xyzsd.dichotomy.trying.function.ExFunction; +import net.xyzsd.dichotomy.trying.function.ExRunnable; import net.xyzsd.dichotomy.trying.function.ExSupplier; import org.junit.jupiter.api.Test; @@ -82,9 +84,9 @@ public void close() throws IOException { private static void assertMessage(final String expectedText, Try tried) { requireNonNull( expectedText ); requireNonNull( tried ); - if (tried instanceof Try.Success) { + if (tried instanceof Success) { throw new AssertionError( "Expected Failure, not " + tried ); - } else if (tried instanceof Try.Failure failure) { + } else if (tried instanceof Failure failure) { if (!Objects.equals( expectedText, failure.err().getMessage() )) { throw new AssertionError( String.format( "Expected '%s' Actual: '%s'", expectedText, @@ -101,9 +103,9 @@ private static void assertException(final Class excepti requireNonNull( exceptionClass ); requireNonNull( tried ); - if (tried instanceof Try.Success) { + if (tried instanceof Success) { throw new AssertionError( "Expected Failure, not " + tried ); - } else if (tried instanceof Try.Failure failure) { + } else if (tried instanceof Failure failure) { if (!Objects.equals( exceptionClass, failure.err().getClass() )) { throw new AssertionError( String.format( "Expected class '%s' Actual: '%s'", exceptionClass.getClass(), @@ -120,9 +122,9 @@ private static void assertSuppressed(final int nSuppressed, Try tried) { if (nSuppressed < 0) {throw new IllegalArgumentException();} requireNonNull( tried ); - if (tried instanceof Try.Success) { + if (tried instanceof Success) { throw new AssertionError( "Expected Failure, not " + tried ); - } else if (tried instanceof Try.Failure failure) { + } else if (tried instanceof Failure failure) { if (nSuppressed != failure.err().getSuppressed().length) { throw new AssertionError( String.format( "Expected %d suppressed exceptions; actual count: %d: %s", nSuppressed, @@ -136,7 +138,7 @@ private static void assertSuppressed(final int nSuppressed, Try tried) { } private static void assertSuccessfulTry(String expectedValue, Try in) { - if(in instanceof Try.Success success) { + if (in instanceof Success success) { assertEquals( expectedValue, success.value() ); } else { throw new AssertionError(); @@ -149,15 +151,15 @@ void ofSupplier() { // test simple suppliers { // success - final Try stn = Try.of( STRING_SUPPLIER_OK::get ); - assertEquals( Try.Success.class, stn.getClass() ); + final Try stn = Try.wrap( STRING_SUPPLIER_OK::get ); + assertEquals( Success.class, stn.getClass() ); assertTrue( stn.contains( SUPPLIED_STRING ) ); } { // fail - final Try stn = Try.of( STRING_SUPPLIER_FAIL::get ); - assertEquals( Try.Failure.class, stn.getClass() ); + final Try stn = Try.wrap( STRING_SUPPLIER_FAIL::get ); + assertEquals( Failure.class, stn.getClass() ); assertFalse( stn.contains( SUPPLIED_STRING ) ); assertMessage( RE.getMessage(), stn ); assertSuppressed( 0, stn ); @@ -166,15 +168,15 @@ void ofSupplier() { // checked suppliers { // success - final Try stn = Try.of( EX_STRING_SUPPLIER_OK ); - assertEquals( Try.Success.class, stn.getClass() ); + final Try stn = Try.wrap( EX_STRING_SUPPLIER_OK ); + assertEquals( Success.class, stn.getClass() ); assertTrue( stn.contains( SUPPLIED_STRING ) ); } { // fail - final Try stn = Try.of( EX_STRING_SUPPLIER_FAIL ); - assertEquals( Try.Failure.class, stn.getClass() ); + final Try stn = Try.wrap( EX_STRING_SUPPLIER_FAIL ); + assertEquals( Failure.class, stn.getClass() ); assertFalse( stn.contains( SUPPLIED_STRING ) ); assertMessage( IOE.getMessage(), stn ); assertSuppressed( 0, stn ); @@ -182,8 +184,8 @@ void ofSupplier() { { // fatal exceptions should be fatal - assertThrows( LinkageError.class, () -> Try.of( FATAL_SUPPLIER_ERROR ) ); - assertThrows( InterruptedException.class, () -> Try.of( FATAL_SUPPLIER_EX ) ); + assertThrows( LinkageError.class, () -> Try.wrap( FATAL_SUPPLIER_ERROR ) ); + assertThrows( InterruptedException.class, () -> Try.wrap( FATAL_SUPPLIER_EX ) ); } } @@ -198,7 +200,7 @@ void withResources1() { return x.toString(); } ); - assertSuccessfulTry("HelloWorld", helloTry); + assertSuccessfulTry( "HelloWorld", helloTry ); } // supplier fail @@ -261,7 +263,7 @@ void withResources2() { return s1.toString() + s2; } ); - assertSuccessfulTry("HelloWorld", helloTry); + assertSuccessfulTry( "HelloWorld", helloTry ); } // function fail + both FAILWRITERs fail at close (suppressed) @@ -284,14 +286,14 @@ void withResources2() { @Test void isFailure() { - assertFalse( Try.Success.of( SUPPLIED_STRING ).isFailure() ); - assertTrue( Try.Failure.of( IOE ).isFailure() ); + assertFalse( Success.of( SUPPLIED_STRING ).isFailure() ); + assertTrue( Failure.of( IOE ).isFailure() ); } @Test void isSuccess() { - assertTrue( Try.Success.of( SUPPLIED_STRING ).isSuccess() ); - assertFalse( Try.Failure.of( IOE ).isSuccess() ); + assertTrue( Success.of( SUPPLIED_STRING ).isSuccess() ); + assertFalse( Failure.of( IOE ).isSuccess() ); } @@ -580,9 +582,9 @@ void ifPredicate() { @Test void contains() { - assertTrue( Try.Success.of( SUPPLIED_STRING ).contains( "SuppliedString" ) ); - assertFalse( Try.Success.of( SUPPLIED_STRING ).contains( "randomstring#*@(#$" ) ); - assertFalse( Try.Failure.of( IOE ).contains( "SuppliedString" ) ); + assertTrue( Success.of( SUPPLIED_STRING ).contains( "SuppliedString" ) ); + assertFalse( Success.of( SUPPLIED_STRING ).contains( "randomstring#*@(#$" ) ); + assertFalse( Failure.of( IOE ).contains( "SuppliedString" ) ); // null : allowed but always false. assertFalse( TRY_SUCCESS.contains( null ) ); assertFalse( TRY_FAILURE.contains( null ) ); @@ -777,26 +779,47 @@ void getOrThrow() { @Test - void randomstuff() { - int div = 1; - final Try integerTry = Try.of( - () -> { - int i = 318794; - i = i / Random.from( RandomGenerator.getDefault() ).nextInt(); - i = i / div; - return i; + void wrap() { + // example: + Try.wrap( () -> { + int i = Random.from( RandomGenerator.getDefault() ).nextInt(); + if (i < 0) { + throw new IOException( "oh no!" ); + } else { + // ... do something + } } ); - final Try asdf = Try.of( - () -> { - int i = 318794; - i = i / Random.from( RandomGenerator.getDefault() ).nextInt(); - i = i / div; - return Empty.getInstance(); - } + // testing + assertThrows( NullPointerException.class, () -> Try.wrap( (ExRunnable) null ) ); + + assertEquals( + Try.ofFailure( IOE ), + Try.wrap( () -> { throw IOE; }) + ); + + assertEquals( + Success.of(), + Try.wrap( () -> { for(int i=0; i<10; i++) { int j = 10+i; }}) ); - // so above would be an int or if /0 then a failure } + + + @Test + void highlyContrivedAndNotVeryGoodExamples() { + final Try result = Try.ofSuccess( 777 ) + .map( i -> i * 1000 ) + .exec( System.out::println ) // prints "777000" + .map( i -> i / 0 ) // the ArithmeticException is caught as a Try.Failure + .exec( System.out::println );// does not exec() because we are a Failure + + // prints "ERROR: java.lang.ArithmeticException: / by zero" + switch(result) { + case Success(Integer i) -> System.out.printf("Operation completed successfully. Value: %d\n", i); + case Failure(Throwable t) -> System.err.printf("ERROR: %s\n", t); + } + } + } \ No newline at end of file