diff --git a/README.md b/README.md deleted file mode 100644 index 992cc5c..0000000 --- a/README.md +++ /dev/null @@ -1,203 +0,0 @@ -Mon -=== - -TypeAlias, Maybe and Result for Java - -[![Sonatype Nexus (Releases)](https://img.shields.io/nexus/r/https/oss.sonatype.org/net.kemitix/mon.svg?style=for-the-badge)](https://oss.sonatype.org/content/repositories/releases/net/kemitix/mon/) -[![Maven Central](https://img.shields.io/maven-central/v/net.kemitix/mon.svg?style=for-the-badge)](https://search.maven.org/#search|ga|1|g%3A"net.kemitix"%20AND%20a%3A"mon") - -[![SonarQube Coverage](https://img.shields.io/sonar/https/sonarcloud.io/net.kemitix%3Amon/coverage.svg?style=for-the-badge)](https://sonarcloud.io/dashboard?id=net.kemitix%3Amon) -[![SonarQube Tech Debt](https://img.shields.io/sonar/https/sonarcloud.io/net.kemitix%3Amon/tech_debt.svg?style=for-the-badge)](https://sonarcloud.io/dashboard?id=net.kemitix%3Amon) - -[![Codacy grade](https://img.shields.io/codacy/grade/d57096b0639d496aba9a7e43e7cf5b4c.svg?style=for-the-badge)](https://app.codacy.com/project/kemitix/mon/dashboard) -[![jPeek](http://i.jpeek.org/net.kemitix/mon/badge.svg)](http://i.jpeek.org/net.kemitix/mon/index.html) - -## Maven - -```xml - - net.kemitix - mon - RELEASE - -``` - -The latest version should be shown above with the nexus and maven-central badges. - -## Usage - -### TypeAlias - -More of a type-wrapper really. It's as close as I could get to a Haskell type alias in Java. - -```java -class Goal extends TypeAlias { - private Goal(final String goal) { - super(goal); - } - public static Goal of(final String goal) { - return new Goal(goal); - } -} -``` - -```java -class Example { - Goal goal = Goal.of("goal"); - void foo(final Goal goal) { - System.out.println("The goal is " + goal.getValue()); - } -} -``` - -### Maybe - -A Monad. - -A non-final substitute for Optional with `peek()` and `stream()` methods. - -```java -class Test { - @Test - public void maybeTests() { - // Constructors: maybe(T), just(T) and nothing() - assertThat(Maybe.maybe(null)).isEqualTo(Maybe.nothing()); - assertThat(Maybe.maybe(1)).isEqualTo(Maybe.just(1)); - // .orElseGet(Supplier) - assertThat(Maybe.nothing().orElseGet(() -> 1)).isEqualTo(1); - assertThat(Maybe.just(1).orElseGet(() -> 2)).isEqualTo(1); - // .orElse(Supplier) - assertThat(Maybe.nothing().orElse(1)).isEqualTo(1); - assertThat(Maybe.just(1).orElse(2)).isEqualTo(1); - // .filter(Predicate) - assertThat(Maybe.just(1).filter(v -> v > 2)).isEqualTo(Maybe.nothing()); - assertThat(Maybe.just(3).filter(v -> v > 2)).isEqualTo(Maybe.just(3)); - assertThat(Maybe.just(1).toOptional()).isEqualTo(Optional.of(1)); - assertThat(Maybe.nothing().toOptional()).isEqualTo(Optional.empty()); - // .fromOptional(Optional) is deprecated - assertThat(Maybe.fromOptional(Optional.of(1))).isEqualTo(Maybe.just(1)); - assertThat(Maybe.fromOptional(Optional.empty())).isEqualTo(Maybe.nothing()); - // An alternative to using .fromOptional(Optional) - assertThat(Optional.of(1).map(Maybe::just).orElse(Maybe::nothing)).isEqualTo(Maybe.just(1)); - assertThat(Optional.empty().map(Maybe::just).orElse(Maybe::nothing)).isEqualTo(Maybe.nothing()); - // .peek(Consumer) - final AtomicInteger reference = new AtomicInteger(0); - assertThat(Maybe.just(1).peek(reference::set)).isEqualTo(Maybe.just(1)); - assertThat(reference).hasValue(1); - assertThat(Maybe.nothing().peek(v -> reference.incrementAndGet())).isEqualTo(Maybe.nothing()); - assertThat(reference).hasValue(1); - // .orElseThrow(Supplier) - assertThatCode(() -> Maybe.just(1).orElseThrow(IllegalStateException::new)).doesNotThrowAnyException(); - assertThatThrownBy(() -> Maybe.nothing().orElseThrow(IllegalStateException::new)).isInstanceOf(IllegalStateException.class); - // .stream() - assertThat(Maybe.just(1).stream()).containsExactly(1); - assertThat(Maybe.nothing().stream()).isEmpty(); - } -} -``` - -### Result - -A Monad. - -A container for method return values that may raise an Exception. Useful for when a checked exceptions can't be added -to the method signature. - -```java -package net.kemitix.mon; - -import net.kemitix.mon.result.Result; - -import java.io.IOException; - -class ResultExample implements Runnable { - - public static void main(String[] args) { - new ResultExample().run(); - } - - @Override - public void run() { - System.out.println("run"); - final Result goodResult = goodMethod(); - if (goodResult.isOkay()) { - doGoodThings(); - } - if (goodResult.isError()) { - notCalled(0); - } - - goodResult.flatMap(number -> convertToString(number)) - .flatMap(str -> stringLength(str)) - .match( - success -> System.out.format("Length is %s%n", success), - error -> System.out.println("Count not determine length") - ); - - final Result badResult = badMethod(); - badResult.match( - success -> notCalled(success), - error -> handleError(error) - ); - } - - private Result goodMethod() { - System.out.println("goodMethod"); - return Result.ok(1); - } - - private void doGoodThings() { - System.out.println("doGoodThings"); - } - - private void notCalled(final Integer success) { - System.out.println("notCalled"); - } - - private Result convertToString(final Integer number) { - System.out.println("convertToString"); - return Result.ok(String.valueOf(number)); - } - - private Result stringLength(final String value) { - System.out.println("stringLength"); - if (value == null) { - return Result.error(new NullPointerException("value is null")); - } - return Result.ok(value.length()); - } - - // doesn't need to declare "throws IOException" - private Result badMethod() { - System.out.println("badMethod"); - return Result.error(new IOException("error")); - } - - private void handleError(final Throwable error) { - System.out.println("handleError"); - throw new RuntimeException("Handled exception", error); - } - -} -``` -Will output: -```text -run -goodMethod -doGoodThings -convertToString -stringLength -Length is 1 -badMethod -handleError -Exception in thread "main" java.lang.RuntimeException: Handled exception - at net.kemitix.mon.ResultExample.handleError(ResultExample.java:72) - at net.kemitix.mon.ResultExample.lambda$run$5(ResultExample.java:34) - at net.kemitix.mon.result.Err.match(Err.java:56) - at net.kemitix.mon.ResultExample.run(ResultExample.java:32) - at net.kemitix.mon.ResultExample.main(ResultExample.java:10) -Caused by: java.io.IOException: error - at net.kemitix.mon.ResultExample.badMethod(ResultExample.java:67) - at net.kemitix.mon.ResultExample.run(ResultExample.java:31) - ... 1 more -``` diff --git a/README.org b/README.org new file mode 100644 index 0000000..411f3e0 --- /dev/null +++ b/README.org @@ -0,0 +1,703 @@ +* Mon + :PROPERTIES: + :CUSTOM_ID: mon + :END: + +** TypeAlias, Maybe and Result for Java. + + [[https://oss.sonatype.org/content/repositories/releases/net/kemitix/mon][file:https://img.shields.io/nexus/r/https/oss.sonatype.org/net.kemitix/mon.svg?style=for-the-badge]] + [[https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22net.kemitix%22%20AND%20a%3A%22mon%22][file:https://img.shields.io/maven-central/v/net.kemitix/mon.svg?style=for-the-badge]] + + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://img.shields.io/sonar/https/sonarcloud.io/net.kemitix%3Amon/coverage.svg?style=for-the-badge#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://img.shields.io/sonar/https/sonarcloud.io/net.kemitix%3Amon/tech_debt.svg?style=for-the-badge#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=sqale_rating#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=alert_status#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=reliability_rating#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=security_rating#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=sqale_index#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=vulnerabilities#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=bugs#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=code_smells#.svg]] + [[https://sonarcloud.io/dashboard?id=net.kemitix%3Amon][file:https://sonarcloud.io/api/project_badges/measure?project=net.kemitix%3Amon&metric=ncloc#.svg]] + + [[https://app.codacy.com/project/kemitix/mon/dashboard][file:https://img.shields.io/codacy/grade/d57096b0639d496aba9a7e43e7cf5b4c.svg?style=for-the-badge]] + [[http://i.jpeek.org/net.kemitix/mon/index.html][file:http://i.jpeek.org/net.kemitix/mon/badge.svg]] + +** Maven + :PROPERTIES: + :CUSTOM_ID: maven + :END: + +#+BEGIN_SRC xml + + net.kemitix + mon + RELEASE + +#+END_SRC + + The latest version should be shown above with the nexus and maven-central + badges or can be found on [[https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22net.kemitix%22%20AND%20a%3A%22mon%22][Maven Central]]. + +** Usage + :PROPERTIES: + :CUSTOM_ID: usage + :END: + +*** TypeAlias + :PROPERTIES: + :CUSTOM_ID: typealias + :END: + + In Haskell it is possible to create an alias for a Type, and to then use + that alias with the same behaviour as the original, except that the compiler + doesn't treat the alias as the same Type and will generate compiler errors + if you try and use them together. e.g.: + + #+BEGIN_SRC haskell + type PhoneNumber = String + type Name = String + type PhoneBook = [(Name,PhoneNumber)] + #+END_SRC + + In Java we don't have the ability to have that true alias, so TypeAlias is + more of a type-wrapper. It's as close as I could get to a Haskell type alias + in Java. + + The benefits of using TypeAlias are: + + - encapsulation of the wrapped type when passing references through code + that doesn't need to access the actual value, but only to pass it on + - type-safe parameters where you would otherwise be passing Strings, + Integers, Lists, or other general classes + - equality and hashcode + - less verbose than implementing your own + + *TypeAlias Example:* + + #+BEGIN_SRC java + class PhoneNumber extends TypeAlias { + private PhoneNumber(final String value) { + super(value); + } + public static PhoneNumber of(final String phoneNumber) { + return new PhoneNumber(phoneNumber); + } + } + #+END_SRC + + *Roll your own:* + + #+BEGIN_SRC java + class PhoneNumber { + private final String value; + private PhoneNumber(final String value) { + this.value = value; + } + public static PhoneNumber of(final String phoneNumber) { + return new PhoneNumber(phoneNumber); + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PhoneNumber that = (PhoneNumber) o; + return Objects.equals(value, that.value); + } + @Override + public int hashCode() { + return Objects.hash(value); + } + public String getValue() { + return value; + } + } + #+END_SRC + + *Lombok:* + + Although, if you are using Lombok, that can be equally terse, both it and + TypeAlias coming in at 8 lines each, compared to 24 for rolling your + own: + + #+BEGIN_SRC java + @Value + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + class PhoneNumber { + private final String value; + public static PhoneNumber of(final String phoneNumber) { + return new PhoneNumber(phoneNumber); + } + } + #+END_SRC + +*** Maybe + :PROPERTIES: + :CUSTOM_ID: maybe + :END: + + Allows specifying that a value may or may not be present. Similar to + =Optional=. =Maybe= provides additional methods that =Optional= doesn't: + =isNothing()=, =stream()=, =ifNothing()= and =match()=. =Maybe= does not + have a =get()= method. + + Unlike =Optional=, when a =map()= results in a =null=, the =Maybe= will + continue to be a =Just=. =Optional= would switch to being empty. [[http://blog.vavr.io/the-agonizing-death-of-an-astronaut/][vavi.io + follows the same behaviour as =Maybe=]]. + + #+BEGIN_SRC java + import net.kemitix.mon.maybe.Maybe; + + import java.util.function.Function; + import java.util.function.Predicate; + + class MaybeExample { + + public static void main(String[] args) { + Maybe.just(countArgs(args)) + .filter(isEven()) + .map(validMessage()) + .match( + just -> System.out.println(just), + () -> System.out.println("Not an valid value") + ); + } + + private static Function validMessage() { + return v -> String.format("Value %d is even", v); + } + + private static Predicate isEven() { + return v -> v % 2 == 0; + } + + private static Integer countArgs(String[] args) { + return args.length; + } + } + #+END_SRC + + In the above example, the number of command line arguments are counted, if + there are an even number of them then a message is created and printed by + the Consumer parameter in the =match= call. If there is an odd number of + arguments, then the filter will return =Maybe.nothing()=, meaning that the + =nothing= drops straight through the map and triggers the Runnable parameter + in the =match= call. + +**** =Maybe= is a Monad: + + #+BEGIN_SRC java + package net.kemitix.mon; + + import net.kemitix.mon.maybe.Maybe; + import org.assertj.core.api.WithAssertions; + import org.junit.Test; + + import java.util.function.Function; + + public class MaybeMonadTest implements WithAssertions { + + private final int v = 1; + private final Function> f = i -> m(i * 2); + private final Function> g = i -> m(i + 6); + + private static Maybe m(int value) { + return Maybe.maybe(value); + } + + @Test + public void leftIdentity() { + assertThat( + m(v).flatMap(f) + ).isEqualTo( + f.apply(v) + ); + } + + @Test + public void rightIdentity() { + assertThat( + m(v).flatMap(x -> m(x)) + ).isEqualTo( + m(v) + ); + } + + @Test + public void associativity() { + assertThat( + m(v).flatMap(f).flatMap(g) + ).isEqualTo( + m(v).flatMap(x -> f.apply(x).flatMap(g)) + ); + } + + } + #+END_SRC + +**** Static Constructors + +***** =static Maybe maybe(T value)= + + Create a Maybe for the value that may or may not be present. + + Where the value is =null=, that is taken as not being present. + + #+BEGIN_SRC java + final Maybe just = Maybe.maybe(1); + final Maybe nothing = Maybe.maybe(null); + #+END_SRC + +***** =static Maybe just(T value)= + + Create a Maybe for the value that is present. + + The =value= must not be =null= or a =NullPointerException= will be thrown. + If you can't prove that the value won't be =null= you should use + =Maybe.maybe(value)= instead. + + #+BEGIN_SRC java + final Maybe just = Maybe.just(1); + #+END_SRC + +***** =static Maybe nothing()= + + Create a Maybe for a lack of a value. + + #+BEGIN_SRC java + final Maybe nothing = Maybe.nothing(); + #+END_SRC + +**** Instance Methods + +***** =Maybe filter(Predicate predicate)= + + Filter a Maybe by the predicate, replacing with Nothing when it fails. + + #+BEGIN_SRC java + final Maybe maybe = Maybe.maybe(getValue()) + .filter(v -> v % 2 == 0); + #+END_SRC + +***** = Maybe map(Function f)= + + Applies the function to the value within the Maybe, returning the result within another Maybe. + + #+BEGIN_SRC java + final Maybe maybe = Maybe.maybe(getValue()) + .map(v -> v * 100); + #+END_SRC + +***** = Maybe flatMap(Function> f)= + + Applies the function to the value within the =Maybe=, resulting in another =Maybe=, then flattens the resulting =Maybe>= into =Maybe=. + + Monad binder maps the Maybe into another Maybe using the binder method f + + #+BEGIN_SRC java + final Maybe maybe = Maybe.maybe(getValue()) + .flatMap(v -> Maybe.maybe(getValueFor(v))); + #+END_SRC + +***** =void match(Consumer just, Runnable nothing)= + + Matches the Maybe, either just or nothing, and performs either the Consumer, for Just, or Runnable for nothing. + + #+BEGIN_SRC java + Maybe.maybe(getValue()) + .match( + just -> workWithValue(just), + () -> nothingToWorkWith() + ); + #+END_SRC + +***** =T orElse(T otherValue)= + + A value to use when Maybe is Nothing. + + #+BEGIN_SRC java + final Integer value = Maybe.maybe(getValue()) + .orElse(1); + #+END_SRC + +***** =T orElseGet(Supplier otherValueSupplier)= + + Provide a value to use when Maybe is Nothing. + + #+BEGIN_SRC java + final Integer value = Maybe.maybe(getValue()) + .orElseGet(() -> getDefaultValue()); + #+END_SRC + +***** =void orElseThrow(Supplier error)= + + Throw the exception if the Maybe is a Nothing. + + #+BEGIN_SRC java + final Integer value = Maybe.maybe(getValue()) + .orElseThrow(() -> new RuntimeException("error")); + #+END_SRC + +***** =Maybe peek(Consumer consumer)= + + Provide the value within the Maybe, if it exists, to the Consumer, and returns this Maybe. Conceptually equivalent to the idea of =ifPresent(...)=. + + #+BEGIN_SRC java + final Maybe maybe = Maybe.maybe(getValue()) + .peek(v -> v.foo()); + #+END_SRC + +***** =void ifNothing(Runnable runnable)= + + Run the runnable if the Maybe is a Nothing, otherwise do nothing. + + #+BEGIN_SRC java + Maybe.maybe(getValue()) + .ifNothing(() -> doSomething()); + #+END_SRC + +***** =Stream stream()= + + Converts the Maybe into either a single value stream or an empty stream. + + #+BEGIN_SRC java + final Stream stream = Maybe.maybe(getValue()) + .stream(); + #+END_SRC + +***** =boolean isJust()= + + Checks if the Maybe is a Just. + + #+BEGIN_SRC java + final boolean isJust = Maybe.maybe(getValue()) + .isJust(); + #+END_SRC + +***** =boolean isNothing()= + + Checks if the Maybe is Nothing. + + #+BEGIN_SRC java + final boolean isNothing = Maybe.maybe(getValue()) + .isNothing(); + #+END_SRC + +***** =Optional toOptional()= + + Convert the Maybe to an Optional. + + #+BEGIN_SRC java + final Optional optional = Maybe.maybe(getValue()) + .toOptional(); + #+END_SRC + +*** Result + :PROPERTIES: + :CUSTOM_ID: result + :END: + + Allows handling error conditions without the need to catch exceptions. + + When a =Result= is returned from a method it will contain one of two values. + Either the actual result, or an error in the form of an =Exception=. The + exception is returned within the =Result= and is not thrown. + + #+BEGIN_SRC java + import net.kemitix.mon.result.Result; + + import java.io.IOException; + + class ResultExample implements Runnable { + + public static void main(final String[] args) { + new ResultExample().run(); + } + + @Override + public void run() { + Result.of(() -> callRiskyMethod()) + .flatMap(state -> doSomething(state)) + .match( + success -> System.out.println(success), + error -> error.printStackTrace() + ); + } + + private String callRiskyMethod() throws IOException { + return "I'm fine"; + } + + private Result doSomething(final String state) { + return Result.of(() -> state + ", it's all good."); + } + + } + #+END_SRC + + In the above example the string ="I'm fine"= is returned by + =callRiskyMethod()= within a successful =Result=. The =.flatMap()= call, + unwraps that =Result= and, as it is a success, passes the contents to + =doSomething()=, which in turn returns a =Result= that the =.flatMap()= call + returns. =match()= is called on the =Result= and, being a success, will call + the success =Consumer=. + + Had =callRiskyMethod()= thrown an exception it would have been caught by the + =Result.of()= method which would have then been an error =Result=. An error + Result would have ignored the =flatMap= and skipped to the =match()= when it + would have called the error =Consumer=. + +**** =Result= is a Monad + + #+BEGIN_SRC java + package net.kemitix.mon; + + import net.kemitix.mon.result.Result; + import org.assertj.core.api.WithAssertions; + import org.junit.Test; + + import java.util.function.Function; + + public class ResultMonadTest implements WithAssertions { + + private final int v = 1; + private final Function> f = i -> r(i * 2); + private final Function> g = i -> r(i + 6); + + private static Result r(int v) { + return Result.ok(v); + } + + @Test + public void leftIdentity() { + assertThat( + r(v).flatMap(f) + ).isEqualTo( + f.apply(v) + ); + } + + @Test + public void rightIdentity() { + assertThat( + r(v).flatMap(x -> r(x)) + ).isEqualTo( + r(v) + ); + } + + @Test + public void associativity() { + assertThat( + r(v).flatMap(f).flatMap(g) + ).isEqualTo( + r(v).flatMap(x -> f.apply(x).flatMap(g)) + ); + } + + } + #+END_SRC + +**** Static Constructors + +***** =static Result of(Callable callable)= + + Create a Result for a output of the Callable. + + If the Callable throws and Exception, then the Result will be an error and + will contain that exception. + + This will be the main starting point for most Results where the callable + could throw an =Exception=. + + #+BEGIN_SRC java + final Result okay = Result.of(() -> 1); + final Result error = Result.of(() -> {throw new RuntimeException();}); + #+END_SRC + +***** =static Result ok(T value)= + + Create a Result for a success. + + Use this where you have a value that you want to place into the Result context. + + #+BEGIN_SRC java + final Result okay = Result.ok(1); + #+END_SRC + +***** =static Result error(Throwable error)= + + Create a Result for an error. + + #+BEGIN_SRC java + final Result error = Result.error(new RuntimeException()); + #+END_SRC + +**** Static Methods + + These static methods provide integration with the =Maybe= class. + + #+BEGIN_SRC java + #+END_SRC + +***** =static Maybe toMaybe(Result result)= + + Creates a =Maybe= from the =Result=, where the =Result= is a success, then + the =Maybe= will contain the value. However, if the =Result= is an error + then the =Maybe= will be nothing. + + #+BEGIN_SRC java + final Result result = Result.of(() -> getValue()); + final Maybe maybe = Result.toMaybe(result); + #+END_SRC + +***** =static Result fromMaybe(Maybe maybe, Supplier error)= + + Creates a =Result= from the =Maybe=, where the =Result= will be an error + if the =Maybe= is nothing. Where the =Maybe= is nothing, then the + =Supplier= will provide the error for the =Result=. + + #+BEGIN_SRC java + final Maybe maybe = Maybe.maybe(getValue()); + final Result result = Result.fromMaybe(maybe, () -> new NoSuchFileException("filename")); + #+END_SRC + +***** =static Result> invert(Maybe> maybeResult)= + + Swaps the =Result= within a =Maybe=, so that =Result= contains a =Maybe=. + + #+BEGIN_SRC java + final Maybe> maybe = Maybe.maybe(Result.of(() -> getValue())); + final Result> result = Result.invert(maybe); + #+END_SRC + +***** =static Result> flatMapMaybe(Result> maybeResult, Function,Result>> f)= + + Applies the function to the contents of a Maybe within the Result. + + #+BEGIN_SRC java + final Result> result = Result.of(() -> Maybe.maybe(getValue())); + final Result> maybeResult = Result.flatMapMaybe(result, maybe -> Result.of(() -> maybe.map(v -> v * 2))); + #+END_SRC + +**** Instance Methods + +***** Result map(Function f) + + Applies the function to the value within the Functor, returning the result + within a Functor. + + #+BEGIN_SRC java + final Result result = Result.of(() -> getValue()) + .map(v -> String.valueOf(v)); + #+END_SRC + +***** Result flatMap(Function> f) + + Returns a new Result consisting of the result of applying the function to + the contents of the Result. + + #+BEGIN_SRC java + final Result result = Result.of(() -> getValue()) + .flatMap(v -> Result.of(() -> String.valueOf(v))); + #+END_SRC + +***** Result andThen(Function> f) + + Maps a Success Result to another Result using a Callable that is able to + throw a checked exception. + + #+BEGIN_SRC java + final Result result = Result.of(() -> getValue()) + .andThen(v -> () -> {throw new IOException();}); + #+END_SRC + +***** void match(Consumer onSuccess, Consumer onError) + + Matches the Result, either success or error, and supplies the appropriate + Consumer with the value or error. + + #+BEGIN_SRC java + Result.of(() -> getValue()) + .match( + success -> System.out.println(success), + error -> System.err.println("error") + ); + #+END_SRC + +***** Result recover(Function> f) + + Provide a way to attempt to recover from an error state. + + #+BEGIN_SRC java + final Result result = Result.of(() -> getValue()) + .recover(e -> Result.of(() -> getSafeValue(e))); + #+END_SRC + +***** Result peek(Consumer consumer) + + Provide the value within the Result, if it is a success, to the Consumer, + and returns this Result. + + #+BEGIN_SRC java + final Result result = Result.of(() -> getValue()) + .peek(v -> System.out.println(v)); + #+END_SRC + +***** Result thenWith(Function> f) + + Perform the continuation with the current Result value then return the + current Result, assuming there was no error in the continuation. + + #+BEGIN_SRC java + final Result result = Result.of(() -> getValue()) + .thenWith(v -> () -> System.out.println(v)) + .thenWith(v -> () -> {throw new IOException();}); + #+END_SRC + +***** Result> maybe(Predicate predicate) + + Wraps the value within the Result in a Maybe, either a Just if the + predicate is true, or Nothing. + + #+BEGIN_SRC java + final Result> result = Result.of(() -> getValue()) + .maybe(v -> v % 2 == 0); + #+END_SRC + +***** T orElseThrow() + + Extracts the successful value from the result, or throws the error + Throwable. + + #+BEGIN_SRC java + final Integer result = Result.of(() -> getValue()) + .orElseThrow(); + #+END_SRC + +***** void onError(Consumer errorConsumer) + + A handler for error states. + + #+BEGIN_SRC java + Result.of(() -> getValue()) + .onError(e -> handleError(e)); + #+END_SRC + +***** boolean isOkay() + + Checks if the Result is a success. + + #+BEGIN_SRC java + final boolean isOkay = Result.of(() -> getValue()) + .isOkay(); + #+END_SRC + +***** boolean isError() + + Checks if the Result is an error. + + #+BEGIN_SRC java + final boolean isError = Result.of(() -> getValue()) + .isError(); + #+END_SRC diff --git a/src/main/java/net/kemitix/mon/maybe/Just.java b/src/main/java/net/kemitix/mon/maybe/Just.java index 9dea530..fd916bf 100644 --- a/src/main/java/net/kemitix/mon/maybe/Just.java +++ b/src/main/java/net/kemitix/mon/maybe/Just.java @@ -97,6 +97,11 @@ final class Just implements Maybe { return Optional.of(value); } + @Override + public T orElseThrow(final Supplier e) throws X { + return value; + } + @Override public Maybe peek(final Consumer consumer) { consumer.accept(value); @@ -113,11 +118,6 @@ final class Just implements Maybe { justMatcher.accept(value); } - @Override - public void orElseThrow(final Supplier e) { - // do not throw - } - @Override public Stream stream() { return Stream.of(value); diff --git a/src/main/java/net/kemitix/mon/maybe/Maybe.java b/src/main/java/net/kemitix/mon/maybe/Maybe.java index 0093c53..e24dec7 100644 --- a/src/main/java/net/kemitix/mon/maybe/Maybe.java +++ b/src/main/java/net/kemitix/mon/maybe/Maybe.java @@ -43,6 +43,9 @@ public interface Maybe extends Functor> { /** * Create a Maybe for the value that is present. * + *

The {@literal value} must not be {@literal null} or a {@literal NullPointerException} will be thrown. + * If you can't prove that the value won't be {@literal null} you should use {@link #maybe(Object)} instead.

+ * * @param value the value, not null * @param the type of the value * @return a Maybe of the value @@ -65,6 +68,8 @@ public interface Maybe extends Functor> { /** * Create a Maybe for the value that may or may not be present. * + *

Where the value is {@literal null}, that is taken as not being present.

+ * * @param value the value, may be null * @param the type of the value * @return a Maybe, either a Just, or Nothing if value is null @@ -114,7 +119,7 @@ public interface Maybe extends Functor> { * A value to use when Maybe is Nothing. * * @param otherValue an alternate value - * @return a Maybe + * @return the value of the Maybe if a Just, otherwise the otherValue */ T orElse(T otherValue); @@ -128,14 +133,15 @@ public interface Maybe extends Functor> { /** * Throw the exception if the Maybe is a Nothing. * - * @param e the exception to throw - * @throws Exception if the Maybe is a Nothing + * @param e the exception to throw + * @param the type of the exception to throw + * @return the value of the Maybe if a Just + * @throws X if the Maybe is nothing */ - @SuppressWarnings("illegalthrows") - void orElseThrow(Supplier e) throws Exception; + T orElseThrow(Supplier e) throws X; /** - * Converts the Maybe into either a single value stream or and empty stream. + * Converts the Maybe into either a single value stream or an empty stream. * * @return a Stream containing the value or nothing. */ diff --git a/src/main/java/net/kemitix/mon/maybe/Nothing.java b/src/main/java/net/kemitix/mon/maybe/Nothing.java index 58f169d..ada237e 100644 --- a/src/main/java/net/kemitix/mon/maybe/Nothing.java +++ b/src/main/java/net/kemitix/mon/maybe/Nothing.java @@ -80,6 +80,11 @@ final class Nothing implements Maybe { return Optional.empty(); } + @Override + public T orElseThrow(final Supplier e) throws X { + throw e.get(); + } + @Override public Maybe peek(final Consumer consumer) { return this; @@ -95,11 +100,6 @@ final class Nothing implements Maybe { nothingMatcher.run(); } - @Override - public void orElseThrow(final Supplier e) throws Exception { - throw e.get(); - } - @Override public Stream stream() { return Stream.empty(); diff --git a/src/main/java/net/kemitix/mon/result/Result.java b/src/main/java/net/kemitix/mon/result/Result.java index 72cafdb..4240f67 100644 --- a/src/main/java/net/kemitix/mon/result/Result.java +++ b/src/main/java/net/kemitix/mon/result/Result.java @@ -91,7 +91,9 @@ public interface Result extends Functor> { } /** - * Creates a Result from the Maybe, where the Result will be an error if the Maybe is Nothing. + * Creates a {@link Maybe} from the Result, where the Result is a success, then the Maybe will contain the value. + * + *

However, if the Result is an error then the Maybe will be nothing.

* * @param result the Result the might contain the value of the Result * @param the type of the Maybe and the Result diff --git a/src/test/java/net/kemitix/mon/MaybeMonadTest.java b/src/test/java/net/kemitix/mon/MaybeMonadTest.java index 28e9e8b..bbb6618 100644 --- a/src/test/java/net/kemitix/mon/MaybeMonadTest.java +++ b/src/test/java/net/kemitix/mon/MaybeMonadTest.java @@ -8,32 +8,39 @@ import java.util.function.Function; public class MaybeMonadTest implements WithAssertions { + private final int v = 1; + private final Function> f = i -> m(i * 2); + private final Function> g = i -> m(i + 6); + + private static Maybe m(int value) { + return Maybe.maybe(value); + } + @Test public void leftIdentity() { - //given - final int value = 1; - final Maybe maybe = Maybe.maybe(value); - final Function> f = i -> Maybe.maybe(i * 2); - //then - assertThat(maybe.flatMap(f)).isEqualTo(f.apply(value)); + assertThat( + m(v).flatMap(f) + ).isEqualTo( + f.apply(v) + ); } @Test public void rightIdentity() { - //given - final Maybe maybe = Maybe.maybe(1); - //then - assertThat(maybe.flatMap(Maybe::maybe)).isEqualTo(maybe); + assertThat( + m(v).flatMap(x -> m(x)) + ).isEqualTo( + m(v) + ); } @Test public void associativity() { - //given - final Maybe maybe = Maybe.maybe(1); - final Function> f = i -> Maybe.maybe(i * 2); - final Function> g = i -> Maybe.maybe(i + 6); - //then - assertThat(maybe.flatMap(f).flatMap(g)).isEqualTo(maybe.flatMap(x -> f.apply(x).flatMap(g))); + assertThat( + m(v).flatMap(f).flatMap(g) + ).isEqualTo( + m(v).flatMap(x -> f.apply(x).flatMap(g)) + ); } } diff --git a/src/test/java/net/kemitix/mon/MaybeTest.java b/src/test/java/net/kemitix/mon/MaybeTest.java index 74c6efe..ae354fc 100644 --- a/src/test/java/net/kemitix/mon/MaybeTest.java +++ b/src/test/java/net/kemitix/mon/MaybeTest.java @@ -51,6 +51,29 @@ public class MaybeTest implements WithAssertions { assertThat(nothing().map(v -> v)).isEqualTo(nothing()); } + @Test + public void mapToNull_thenJustNull() { + //given + final Maybe maybe = just(1); + //when + final Maybe result = maybe.map(x -> null); + //then + result.match( + just -> assertThat(just).isNull(), + () -> fail("mapped to a null, not a Nothing - use flatMap() to convert to Nothing in null") + ); + } + + @Test + public void optional_mapToNull_thenJustNull() { + //given + final Optional optional = Optional.ofNullable(1); + //when + final Optional result = optional.map(x -> null); + //then + assertThat(result.isPresent()).isFalse(); + } + @Test public void justHashCode() { assertThat(just(1).hashCode()).isNotEqualTo(just(2).hashCode()); @@ -104,10 +127,20 @@ public class MaybeTest implements WithAssertions { } @Test - public void justOrThrow() { + public void justOrThrowDoesNotThrow() { assertThatCode(() -> just(1).orElseThrow(IllegalStateException::new)).doesNotThrowAnyException(); } + @Test + public void justOrThrowReturnsValue() { + //given + final Maybe maybe = just(1); + //when + final Integer result = maybe.orElseThrow(() -> new RuntimeException()); + //then + assertThat(result).isEqualTo(1); + } + @Test public void nothingOrThrow() { assertThatThrownBy(() -> nothing().orElseThrow(IllegalStateException::new)).isInstanceOf( @@ -133,8 +166,8 @@ public class MaybeTest implements WithAssertions { @Test public void justFlatMap() { //given - final Maybe just1 = Maybe.just(1); - final Maybe just2 = Maybe.just(2); + final Maybe just1 = just(1); + final Maybe just2 = just(2); //when final Maybe result = just1.flatMap(v1 -> just2.flatMap(v2 -> @@ -147,8 +180,8 @@ public class MaybeTest implements WithAssertions { @Test public void nothingFlatMap() { //given - final Maybe nothing1 = Maybe.nothing(); - final Maybe nothing2 = Maybe.nothing(); + final Maybe nothing1 = nothing(); + final Maybe nothing2 = nothing(); //when final Maybe result = nothing1.flatMap(v1 -> nothing2.flatMap(v2 -> @@ -161,8 +194,8 @@ public class MaybeTest implements WithAssertions { @Test public void justNothingFlatMap() { //given - final Maybe just1 = Maybe.just(1); - final Maybe nothing2 = Maybe.nothing(); + final Maybe just1 = just(1); + final Maybe nothing2 = nothing(); //when final Maybe result = just1.flatMap(v1 -> nothing2.flatMap(v2 -> @@ -175,7 +208,7 @@ public class MaybeTest implements WithAssertions { @Test public void just_ifNothing_isIgnored() { //given - final Maybe just = Maybe.just(1); + final Maybe just = just(1); final AtomicBoolean capture = new AtomicBoolean(false); //when just.ifNothing(() -> capture.set(true)); @@ -186,7 +219,7 @@ public class MaybeTest implements WithAssertions { @Test public void nothing_ifNothing_isCalled() { //given - final Maybe nothing = Maybe.nothing(); + final Maybe nothing = nothing(); final AtomicBoolean capture = new AtomicBoolean(false); //when nothing.ifNothing(() -> capture.set(true)); @@ -197,7 +230,7 @@ public class MaybeTest implements WithAssertions { @Test public void just_whenMatch_thenJustTriggers() { //given - final Maybe maybe = Maybe.just(1); + final Maybe maybe = just(1); //then maybe.match( just -> assertThat(just).isEqualTo(1), @@ -208,7 +241,7 @@ public class MaybeTest implements WithAssertions { @Test public void nothing_whenMatch_thenNothingTriggers() { //given - final Maybe maybe = Maybe.nothing(); + final Maybe maybe = nothing(); final AtomicBoolean flag = new AtomicBoolean(false); //when maybe.match( diff --git a/src/test/java/net/kemitix/mon/OptionalMonadTest.java b/src/test/java/net/kemitix/mon/OptionalMonadTest.java new file mode 100644 index 0000000..8284a81 --- /dev/null +++ b/src/test/java/net/kemitix/mon/OptionalMonadTest.java @@ -0,0 +1,46 @@ +package net.kemitix.mon; + +import org.assertj.core.api.WithAssertions; +import org.junit.Test; + +import java.util.Optional; +import java.util.function.Function; + +public class OptionalMonadTest implements WithAssertions { + + private final int v = 1; + private final Function> f = i -> o(i * 2); + private final Function> g = i -> o(i + 6); + + private static Optional o(int value) { + return Optional.ofNullable(value); + } + + @Test + public void leftIdentity() { + assertThat( + o(v).flatMap(f) + ).isEqualTo( + f.apply(v) + ); + } + + @Test + public void rightIdentity() { + assertThat( + o(v).flatMap(x -> o(x)) + ).isEqualTo( + o(v) + ); + } + + @Test + public void associativity() { + assertThat( + o(v).flatMap(f).flatMap(g) + ).isEqualTo( + o(v).flatMap(x -> f.apply(x).flatMap(g)) + ); + } + +} diff --git a/src/test/java/net/kemitix/mon/ResultMonadTest.java b/src/test/java/net/kemitix/mon/ResultMonadTest.java index 8a8470f..d82bd71 100644 --- a/src/test/java/net/kemitix/mon/ResultMonadTest.java +++ b/src/test/java/net/kemitix/mon/ResultMonadTest.java @@ -8,32 +8,39 @@ import java.util.function.Function; public class ResultMonadTest implements WithAssertions { + private final int v = 1; + private final Function> f = i -> r(i * 2); + private final Function> g = i -> r(i + 6); + + private static Result r(int v) { + return Result.ok(v); + } + @Test public void leftIdentity() { - //given - final int value = 1; - final Result result = Result.ok(value); - final Function> f = i -> Result.ok(i * 2); - //then - assertThat(result.flatMap(f)).isEqualTo(f.apply(value)); + assertThat( + r(v).flatMap(f) + ).isEqualTo( + f.apply(v) + ); } @Test public void rightIdentity() { - //given - final Result result = Result.ok(1); - //then - assertThat(result.flatMap(Result::ok)).isEqualTo(result); + assertThat( + r(v).flatMap(x -> r(x)) + ).isEqualTo( + r(v) + ); } @Test public void associativity() { - //given - final Result result = Result.ok(1); - final Function> f = i -> Result.ok(i * 2); - final Function> g = i -> Result.ok(i + 6); - //then - assertThat(result.flatMap(f).flatMap(g)).isEqualTo(result.flatMap(x -> f.apply(x).flatMap(g))); + assertThat( + r(v).flatMap(f).flatMap(g) + ).isEqualTo( + r(v).flatMap(x -> f.apply(x).flatMap(g)) + ); } }