From e0eb0614c321fd8f030cc7560f5a1a94fa08e0ef Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 10 Dec 2017 16:11:12 +0000 Subject: [PATCH] Add `Maybe`, `Just`, `Nothing` --- CHANGELOG | 1 + README.md | 34 +++++ src/main/java/net/kemitix/mon/Just.java | 94 ++++++++++++ src/main/java/net/kemitix/mon/Maybe.java | 145 +++++++++++++++++++ src/main/java/net/kemitix/mon/Nothing.java | 75 ++++++++++ src/test/java/net/kemitix/mon/MaybeTest.java | 141 ++++++++++++++++++ 6 files changed, 490 insertions(+) create mode 100644 src/main/java/net/kemitix/mon/Just.java create mode 100644 src/main/java/net/kemitix/mon/Maybe.java create mode 100644 src/main/java/net/kemitix/mon/Nothing.java create mode 100644 src/test/java/net/kemitix/mon/MaybeTest.java diff --git a/CHANGELOG b/CHANGELOG index 5b7d0cd..bbde639 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ CHANGELOG ----- * Restore public access for `TypeAlias.getValue()` +* Add `Maybe`, `Just`, `Nothing` 0.3.0 ----- diff --git a/README.md b/README.md index dd342cd..c823d44 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,37 @@ void foo(final Goal goal) { System.out.println("The goal is " + goal.getValue()); } ``` + +### Maybe (Just & Nothing) + +```java +assertThat(Maybe.maybe(null)).isEqualTo(Maybe.nothing()); +assertThat(Maybe.maybe(1)).isEqualTo(Maybe.just(1)); +assertThat(Maybe.nothing() + .orElseGet(() -> 1)).isEqualTo(1); +assertThat(Maybe.just(1) + .orElseGet(() -> 2)).isEqualTo(1); +assertThat(Maybe.nothing() + .orElse(1)).isEqualTo(1); +assertThat(Maybe.just(1) + .orElse(2)).isEqualTo(1); +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()); +assertThat(Maybe.fromOptional(Optional.of(1))).isEqualTo(Maybe.just(1)); +assertThat(Maybe.fromOptional(Optional.empty())).isEqualTo(Maybe.nothing()); +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); +assertThatCode(() -> Maybe.just(1).orElseThrow(IllegalStateException::new)) + .doesNotThrowAnyException(); +assertThatThrownBy(() -> Maybe.nothing().orElseThrow(IllegalStateException::new)) + .isInstanceOf(IllegalStateException.class); +``` diff --git a/src/main/java/net/kemitix/mon/Just.java b/src/main/java/net/kemitix/mon/Just.java new file mode 100644 index 0000000..9437bc7 --- /dev/null +++ b/src/main/java/net/kemitix/mon/Just.java @@ -0,0 +1,94 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2017 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * A Maybe where a value is present. + * + * @param the type of the content + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +public final class Just implements Maybe { + + private final T value; + + @Override + public Maybe map(final Function f) { + return new Just<>(f.apply(value)); + } + + @Override + public boolean equals(final Object other) { + return other instanceof Just && Objects.equals(this.value, ((Just) other).value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public T orElseGet(final Supplier supplier) { + return value; + } + + @Override + public T orElse(final T otherValue) { + return this.value; + } + + @Override + public Maybe filter(final Predicate predicate) { + if (predicate.test(value)) { + return this; + } + return Maybe.nothing(); + } + + @Override + public Optional toOptional() { + return Optional.of(value); + } + + @Override + public Maybe peek(final Consumer consumer) { + consumer.accept(value); + return this; + } + + @Override + public void orElseThrow(final Supplier e) { + // do not throw + } +} diff --git a/src/main/java/net/kemitix/mon/Maybe.java b/src/main/java/net/kemitix/mon/Maybe.java new file mode 100644 index 0000000..3211d0c --- /dev/null +++ b/src/main/java/net/kemitix/mon/Maybe.java @@ -0,0 +1,145 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2017 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon; + +import lombok.NonNull; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * A value that may or may not be present. + * + * @param the type of the content of the Just + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +public interface Maybe extends Functor> { + + /** + * Create a Maybe for the value that is present. + * + * @param value the value, not null + * @param the type of the value + * + * @return a Maybe of the value + */ + static Maybe just(@NonNull final T value) { + return new Just<>(value); + } + + /** + * Create a Maybe for a lack of a value. + * + * @param the type of the missing value + * + * @return an empty Maybe + */ + @SuppressWarnings("unchecked") + static Maybe nothing() { + return (Maybe) Nothing.INSTANCE; + } + + /** + * Create a Maybe for the value that may or may not be 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 + */ + static Maybe maybe(final T value) { + if (value == null) { + return nothing(); + } + return just(value); + } + + /** + * Create a Maybe from an {@link Optional}. + * + * @param optional the Optional + * @param the type of the Optional + * + * @return a Maybe + */ + static Maybe fromOptional(final Optional optional) { + return optional.map(Maybe::maybe) + .orElse(nothing()); + } + + /** + * Provide a value to use when Maybe is Nothing. + * + * @param supplier supplier for an alternate value + * + * @return a Maybe + */ + T orElseGet(Supplier supplier); + + /** + * A value to use when Maybe is Nothing. + * + * @param otherValue an alternate value + * + * @return a Maybe + */ + T orElse(T otherValue); + + /** + * Filter a Maybe by the predicate, replacing with Nothing when it fails. + * + * @param predicate the test + * + * @return the Maybe, or Nothing if the test returns false + */ + Maybe filter(Predicate predicate); + + /** + * Convert the Maybe to an {@link Optional}. + * + * @return an Optional containing a value for a Just, or empty for a Nothing + */ + Optional toOptional(); + + /** + * Provide the value within the Maybe, if it exists, to the Supplier, and returns the Maybe. + * + * @param consumer the Consumer to the value if present + * + * @return the Maybe + */ + Maybe peek(Consumer consumer); + + /** + * Throw the exception if the Maybe is a Nothing. + * + * @param e the exception to throw + * + * @throws Exception if the Maybe is a Nothing + */ + @SuppressWarnings("illegalthrows") + void orElseThrow(Supplier e) throws Exception; + +} diff --git a/src/main/java/net/kemitix/mon/Nothing.java b/src/main/java/net/kemitix/mon/Nothing.java new file mode 100644 index 0000000..f484b1f --- /dev/null +++ b/src/main/java/net/kemitix/mon/Nothing.java @@ -0,0 +1,75 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2017 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon; + +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * A Maybe where no value is present. + * + * @param the type of the missing content + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +public final class Nothing implements Maybe { + + protected static final Maybe INSTANCE = new Nothing<>(); + + @Override + public Maybe map(final Function f) { + return this; + } + + @Override + public T orElseGet(final Supplier supplier) { + return supplier.get(); + } + + @Override + public T orElse(final T otherValue) { + return otherValue; + } + + @Override + public Maybe filter(final Predicate predicate) { + return this; + } + + @Override + public Optional toOptional() { + return Optional.empty(); + } + + @Override + public Maybe peek(final Consumer consumer) { + return this; + } + + @Override + public void orElseThrow(final Supplier e) throws Exception { + throw e.get(); + } +} diff --git a/src/test/java/net/kemitix/mon/MaybeTest.java b/src/test/java/net/kemitix/mon/MaybeTest.java new file mode 100644 index 0000000..f0a241a --- /dev/null +++ b/src/test/java/net/kemitix/mon/MaybeTest.java @@ -0,0 +1,141 @@ +package net.kemitix.mon; + +import org.junit.Test; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +import static net.kemitix.mon.Maybe.just; +import static net.kemitix.mon.Maybe.maybe; +import static net.kemitix.mon.Maybe.nothing; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class MaybeTest { + + private static Predicate eq(final T value) { + return v -> Objects.equals(value, v); + } + + @Test + public void documentation() { + assertThat(Maybe.maybe(null)).isEqualTo(Maybe.nothing()); + assertThat(Maybe.maybe(1)).isEqualTo(Maybe.just(1)); + assertThat(Maybe.nothing() + .orElseGet(() -> 1)).isEqualTo(1); + assertThat(Maybe.just(1) + .orElseGet(() -> 2)).isEqualTo(1); + assertThat(Maybe.nothing() + .orElse(1)).isEqualTo(1); + assertThat(Maybe.just(1) + .orElse(2)).isEqualTo(1); + 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()); + assertThat(Maybe.fromOptional(Optional.of(1))).isEqualTo(Maybe.just(1)); + assertThat(Maybe.fromOptional(Optional.empty())).isEqualTo(Maybe.nothing()); + 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); + assertThatCode(() -> Maybe.just(1) + .orElseThrow(IllegalStateException::new)).doesNotThrowAnyException(); + assertThatThrownBy(() -> Maybe.nothing() + .orElseThrow(IllegalStateException::new)).isInstanceOf( + IllegalStateException.class); + } + + @Test + public void justMustBeNonNull() { + assertThatNullPointerException().isThrownBy(() -> just(null)) + .withMessage("value"); + } + + @Test + public void nothingReusesTheSameInstance() { + assertThat(nothing()).isSameAs(nothing()); + } + + @Test + public void maybeAllowsNull() { + assertThat(just(1)).isEqualTo(maybe(1)); + assertThat(nothing()).isEqualTo(maybe(null)); + } + + @Test + public void map() { + assertThat(just(1).map(v -> v + 1)).isEqualTo(just(2)); + assertThat(nothing().map(v -> v)).isEqualTo(nothing()); + } + + @Test + public void testHashCode() { + assertThat(just(1).hashCode()).isEqualTo(Objects.hashCode(1)); + } + + @Test + public void orElseGet() { + assertThat(just(1).orElseGet(() -> -1)).isEqualTo(1); + assertThat(nothing().orElseGet(() -> -1)).isEqualTo(-1); + } + + @Test + public void orElse() { + assertThat(just(1).orElse(-1)).isEqualTo(1); + assertThat(nothing().orElse(-1)).isEqualTo(-1); + } + + @Test + public void filter() { + assertThat(just(1).filter(eq(1))).isEqualTo(just(1)); + assertThat(just(1).filter(eq(0))).isEqualTo(nothing()); + assertThat(nothing().filter(eq(1))).isEqualTo(nothing()); + } + + @Test + public void toOptional() { + assertThat(just(1).toOptional()).isEqualTo(Optional.of(1)); + assertThat(Maybe.nothing() + .toOptional()).isEqualTo(Optional.empty()); + } + + @Test + public void fromOptional() { + assertThat(Maybe.fromOptional(Optional.of(1))).isEqualTo(just(1)); + assertThat(Maybe.fromOptional(Optional.empty())).isEqualTo(Maybe.nothing()); + } + + + @Test + public void peek() { + final AtomicInteger ref = new AtomicInteger(0); + assertThat(just(1).peek(x -> ref.incrementAndGet())).isEqualTo(just(1)); + assertThat(ref.get()).isEqualTo(1); + + assertThat(nothing().peek(x -> ref.incrementAndGet())).isEqualTo(nothing()); + assertThat(ref.get()).isEqualTo(1); + } + + @Test + public void justOrThrow() { + assertThatCode(() -> just(1).orElseThrow(IllegalStateException::new)).doesNotThrowAnyException(); + } + + @Test + public void nothingOrThrow() { + assertThatThrownBy(() -> nothing().orElseThrow(IllegalStateException::new)).isInstanceOf( + IllegalStateException.class); + } +}