* Optional.of(value) - * .map(str -> Maybe.fromResolver(() -> decode(str))); + * .map(str -> Maybe.fromResolver(() -> decode(str))); ** Is equivalent to *
@@ -166,7 +166,7 @@ public staticFunction> part * In other words, the following code ** Optional.of(value) - * .map(msg -> Maybe.fromEffect(() -> sendMessage(msg))); + * .map(msg -> Maybe.fromEffect(() -> sendMessage(msg))); ** Is equivalent to *@@ -341,7 +341,8 @@ public boolean equals(final Object obj) { return true; } - if (obj instanceof final Maybe> other) { + if (obj instanceof Maybe>) { // NOSONAR + final var other = (Maybe>) obj; return other.toOptional().equals(value); } @@ -370,7 +371,7 @@ public int hashCode() { public String toString() { return value .map(Object::toString) - .map("Maybe[%s]"::formatted) + .map(it -> String.format("Maybe[%s]", it)) .orElse("Maybe.nothing"); } } diff --git a/src/main/java/io/github/joselion/maybe/util/Either.java b/src/main/java/io/github/joselion/maybe/util/Either.java index d5ff374..b60b657 100644 --- a/src/main/java/io/github/joselion/maybe/util/Either.java +++ b/src/main/java/io/github/joselion/maybe/util/Either.java @@ -21,7 +21,7 @@ * @author Jose Luis Leon * @since v3.0.0 */ -public sealed interface Either{ +public interface Either { /** * Factory method to create an {@code Either} instance that contains a @@ -174,17 +174,23 @@ default Optional rightToOptional() { * * @param the {@code Left} data type * @param the {@code Right} data type - * @param value the left value */ - record Left (L value) implements Either { + class Left implements Either { + + private final L value; /** * Compact constructor to validate the value is not null. * * @param value the value of the instance */ - public Left { + public Left(final L value) { Objects.requireNonNull(value, "An Either cannot be created with a null value"); + this.value = value; + } + + L value() { + return this.value; } @Override @@ -242,7 +248,8 @@ public boolean equals(final Object obj) { return true; } - if (obj instanceof final Left, ?> left) { + if (obj instanceof Left, ?>) { // NOSONAR + final var left = (Left, ?>) obj; return this.value.equals(left.leftOrNull()); } @@ -256,7 +263,7 @@ public int hashCode() { @Override public String toString() { - return "Either[Left: %s]".formatted(this.value); + return String.format("Either[Left: %s]", this.value); } } @@ -265,17 +272,23 @@ public String toString() { * * @param the {@code Left} data type * @param the {@code Right} data type - * @param value the right value */ - record Right (R value) implements Either { + class Right implements Either { + + private final R value; /** * Compact constructor to validate the value is not null. * * @param value the value of the instance */ - public Right { + public Right(final R value) { Objects.requireNonNull(value, "An Either cannot be created with a null value"); + this.value = value; + } + + R value() { + return this.value; } @Override @@ -333,7 +346,8 @@ public boolean equals(final Object obj) { return true; } - if (obj instanceof final Right, ?> right) { + if (obj instanceof Right, ?>) { // NOSONAR + final var right = (Right, ?>) obj; return this.value.equals(right.rightOrNull()); } @@ -347,7 +361,7 @@ public int hashCode() { @Override public String toString() { - return "Either[Right: %s]".formatted(this.value); + return String.format("Either[Right: %s]", this.value); } } } diff --git a/src/main/java17/io/github/joselion/maybe/Maybe.java b/src/main/java17/io/github/joselion/maybe/Maybe.java new file mode 100644 index 0000000..1849c5a --- /dev/null +++ b/src/main/java17/io/github/joselion/maybe/Maybe.java @@ -0,0 +1,376 @@ +package io.github.joselion.maybe; + +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.Nullable; + +import io.github.joselion.maybe.util.function.ThrowingConsumer; +import io.github.joselion.maybe.util.function.ThrowingFunction; +import io.github.joselion.maybe.util.function.ThrowingRunnable; +import io.github.joselion.maybe.util.function.ThrowingSupplier; + +/** + * Maybe is a monadic wrapper that may contain a value. Its rich API allows to + * process throwing operations in a functional way leveraging {@link Optional} + * to unwrap the possible contained value. + * + * @param the type of the wrapped value + * + * @author Jose Luis Leon + * @since v0.1.0 + */ +public final class Maybe { + + private final Optional value; + + private Maybe(final @Nullable T value) { + this.value = Optional.ofNullable(value); + } + + /** + * Internal use only. + * + * @return the possible wrapped value + */ + Optional value() { + return value; + } + + /** + * Creates a {@link Maybe} wrapper of the given value. If the value is + * {@code null}, it returns a {@link #nothing()}. + * + * @param the type of the value + * @param value the value be wrapped + * @return a {@code Maybe} wrapping the value if it's non-{@code null}, + * {@link #nothing()} otherwise + */ + public static Maybe just(final T value) { + return new Maybe<>(value); + } + + /** + * Creates a {@link Maybe} wrapper with nothing on it. This means the wrapper + * does not contains a value because an exception may have occurred. + * + * @param the type of the value + * @return a {@code Maybe} with nothing + */ + public static Maybe nothing() { + return new Maybe<>(null); + } + + /** + * Creates a {@link Maybe} wrapper of the given value if the optional is not + * empty. Returns a {@link #nothing()} otherwise. + * + * This is a convenience creator that would be equivalent to: + *
+ * Maybe.just(opt) + * .resolve(Optional::get) + * .toMaybe(); + *+ * + * @paramthe type of the value + * @param value an optional value to create the wrapper from + * @return a {@code Maybe} wrapping the value if it's not empty. + * {@link #nothing()} otherwise + */ + public static Maybe fromOptional(final Optional value) { + return new Maybe<>(value.orElse(null)); + } + + /** + * Resolves the value of a throwing operation using a {@link ThrowingSupplier} + * expression. Returning then a {@link ResolveHandler} which allows to handle + * the possible error and return a safe value. + * + * @param the type of the value returned by the {@code resolver} + * @param the type of exception the {@code resolver} may throw + * @param resolver the checked supplier operation to resolve + * @return a {@link ResolveHandler} with either the value resolved or the thrown + * exception to be handled + */ + public static ResolveHandler fromResolver(final ThrowingSupplier resolver) { + try { + return ResolveHandler.ofSuccess(resolver.get()); + } catch (Throwable e) { // NOSONAR + @SuppressWarnings("unchecked") + final var error = (E) e; + return ResolveHandler.ofError(error); + } + } + + /** + * Runs an effect that may throw an exception using a {@link ThrowingRunnable} + * expression. Returning then an {@link EffectHandler} which allows to handle + * the possible error. + * + * @param the type of exception the {@code effect} may throw + * @param effect the checked runnable operation to execute + * @return an {@link EffectHandler} with either the thrown exception to be + * handled or nothing + */ + public static EffectHandler fromEffect(final ThrowingRunnable effect) { + try { + effect.run(); + return EffectHandler.empty(); + } catch (Throwable e) { // NOSONAR + @SuppressWarnings("unchecked") + final var error = (E) e; + return EffectHandler.ofError(error); + } + } + + /** + * Convenience partial application of a {@code resolver}. This method creates + * a function that receives an {@code S} value which can be used to produce a + * {@link ResolveHandler} once applied. This is specially useful when we want + * to create a {@link Maybe} from a callback argument, like on a + * {@link Optional#map(Function)} for instance. + * + * In other words, the following code + *
+ * Optional.of(value) + * .map(str -> Maybe.fromResolver(() -> decode(str))); + *+ * Is equivalent to + *+ * Optional.of(value) + * .map(Maybe.partialResolver(this::decode)); + *+ * + * @paramthe type of the value the returned function receives + * @paramthe type of the value to be resolved + * @param the type of the error the resolver may throw + * @param resolver a checked function that receives an {@code S} value and + * returns a {@code T} value + * @return a partially applied {@link ResolveHandler}. This means, a function + * that receives an {@code S} value, and produces a {@code ResolveHandler } + */ + public static Function> partialResolver( + final ThrowingFunctionresolver + ) { + return value -> Maybe.fromResolver(() -> resolver.apply(value)); + } + + /** + * Convenience partial application of an {@code effect}. This method creates + * a function that receives an {@code S} value which can be used to produce + * an {@link EffectHandler} once applied. This is specially useful when we + * want to create a {@link Maybe} from a callback argument, like on a + * {@link Optional#map(Function)} for instance. + *+ * In other words, the following code + *
+ * Optional.of(value) + * .map(msg -> Maybe.fromEffect(() -> sendMessage(msg))); + *+ * Is equivalent to + *+ * Optional.of(value) + * .map(Maybe.partialEffect(this::sendMessage)); + *+ * + * @paramthe type of the value the returned function receives + * @paramthe type of the error the resolver may throw + * @param effect a checked consumer that receives an {@code S} value + * @return a partially applied {@link EffectHandler}. This means, a function + * that receives an {@code S} value, and produces an {@code EffectHandler } + */ + public static Function> partialEffect( + final ThrowingConsumereffect + ) { + return value -> Maybe.fromEffect(() -> effect.accept(value)); + } + + /** + * Prepare an {@link AutoCloseable} resource to use in a resolver or effect. + * The resource will be automatically closed after the operation is finished, + * just like a common try-with-resources statement. + * + * @paramthe type of the resource. Extends from {@link AutoCloseable} + * @param the type of error the holder may have + * @param resource the {@link AutoCloseable} resource to prepare + * @return a {@link ResourceHolder} which let's you choose to resolve a value + * or run an effect using the prepared resource + */ + public static ResourceHolder withResource(final R resource) { + return ResourceHolder.from(resource); + } + + /** + * If present, maps the value to another using the provided mapper function. + * Otherwise, ignores the mapper and returns {@link #nothing()}. + * + * @param the type the value will be mapped to + * @param mapper the mapper function + * @return a {@code Maybe} with the mapped value if present, + * {@link #nothing()} otherwise + */ + public Maybe map(final Function mapper) { + return value.map(mapper) + .map(Maybe::just) + .orElseGet(Maybe::nothing); + } + + /** + * If present, maps the value to another using the provided mapper function. + * Otherwise, ignores the mapper and returns {@link #nothing()}. + * + * This method is similar to {@link #map(Function)}, but the mapping function is + * one whose result is already a {@code Maybe}, and if invoked, flatMap does not + * wrap it within an additional {@code Maybe}. + * + * @param the type the value will be mapped to + * @param mapper the mapper function + * @return a {@code Maybe} with the mapped value if present, + * {@link #nothing()} otherwise + */ + public Maybe flatMap(final Function > mapper) { + return value.map(mapper) + .orElseGet(Maybe::nothing); + } + + /** + * Chain the {@code Maybe} with another resolver, if and only if the previous + * operation was handled with no errors. The value of the previous operation + * is passed as argument of the {@link ThrowingFunction}. + * + * @param the type of value returned by the next operation + * @param the type of exception the new resolver may throw + * @param resolver a checked function that receives the current value and + * resolves another + * @return a {@link ResolveHandler} with either the resolved value, or the + * thrown exception to be handled + */ + @SuppressWarnings("unchecked") + public ResolveHandler resolve(final ThrowingFunction resolver) { + try { + return value + .map(Maybe.partialResolver(resolver)) + .orElseThrow(); + } catch (final NoSuchElementException error) { + return ResolveHandler.ofError((E) error); + } + } + + /** + * Chain the {@code Maybe} with another effect, if and only if the previous + * operation was handled with no errors. + * + * @param the type of exception the new effect may throw + * @param effect the checked runnable operation to execute next + * @return an {@link EffectHandler} with either the thrown exception to be + * handled or nothing + */ + @SuppressWarnings("unchecked") + public EffectHandler runEffect(final ThrowingConsumer effect) { + try { + return value + .map(Maybe.partialEffect(effect)) + .orElseThrow(); + } catch (final NoSuchElementException error) { + return EffectHandler.ofError((E) error); + } + } + + /** + * If the value is present, cast the value to another type. In case of an + * exception during the cast, a Maybe with {@link #nothing()} is returned. + * + * @param the type that the value will be cast to + * @param type the class instance of the type to cast + * @return a new {@code Maybe} with the cast value if it can be cast, + * {@link #nothing()} otherwise + */ + public Maybe cast(final Class type) { + try { + final var newValue = type.cast(value.orElseThrow()); + return Maybe.just(newValue); + } catch (final ClassCastException error) { + return nothing(); + } + } + + /** + * Checks if the {@code Maybe} has a value. + * + * @return true if the value is present, false otherwise + */ + public boolean hasValue() { + return value.isPresent(); + } + + /** + * Checks if the {@code Maybe} has nothing. That is, when no value is present. + * + * @return true if the value is NOT present, false otherwise + */ + public boolean hasNothing() { + return value.isEmpty(); + } + + /** + * Safely unbox the value as an {@link Optional} which may or may not contain + * a value. + * + * @return an optional with the value, if preset. An empty optional otherwise + */ + public Optional toOptional() { + return value; + } + + /** + * Checks if some other object is equal to this {@code Maybe}. For two objects + * to be equal they both must: + * + *
+ * + * @param obj an object to be tested for equality + * @return {@code true} if the other object is "equal to" this object, + * {@code false} otherwise + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj instanceof final Maybe> other) { + return other.toOptional().equals(value); + } + + return false; + } + + /** + * Returns the hash code of the value, if present, otherwise {@code 0} (zero) + * if no value is present. + * + * @return hash code value of the present value or {@code 0} if no value is present + */ + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Returns a non-empty string representation of this {@code Maybe} suitable + * for debugging. The exact presentation format is unspecified and may vary + * between implementations and versions. + * + * @return the string representation of this instance + */ + @Override + public String toString() { + return value + .map(Object::toString) + .map("Maybe[%s]"::formatted) + .orElse("Maybe.nothing"); + } +} diff --git a/src/main/java17/io/github/joselion/maybe/util/Either.java b/src/main/java17/io/github/joselion/maybe/util/Either.java new file mode 100644 index 0000000..d5ff374 --- /dev/null +++ b/src/main/java17/io/github/joselion/maybe/util/Either.java @@ -0,0 +1,353 @@ +package io.github.joselion.maybe.util; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * Either is a monadic wrapper that contains one of two possible values which + * are represented as {@code Left} or {@code Right}. the values can be of + * different types, and the API allows to safely transform an unwrap the value. + * + * The sealed interface implementation ensures only one of the two can be + * present at the same time. + * + * @param- Be an instance of {@code Maybe}
+ *- Contain a values equal to via {@code equals()} comparation
+ *the {@code Left} data type + * @param the {@code Right} data type + * + * @author Jose Luis Leon + * @since v3.0.0 + */ +public sealed interface Either { + + /** + * Factory method to create an {@code Either} instance that contains a + * {@code Left} value. + * + * @param the type of the left value + * @param the type of the right value + * @param value the value to use as left in the {@code Either} instance + * @return an {@code Either} instance with a left value + */ + static Either ofLeft(final L value) { + return new Left<>(value); + } + + /** + * Factory method to create an {@code Either} instance that contains a + * {@code Right} value. + * + * @param the type of the left value + * @param the type of the right value + * @param value the value to use as right in the {@code Either} instance + * @return an {@code Either} instance with a right value + */ + static Either ofRight(final R value) { + return new Right<>(value); + } + + /** + * Returns true if the {@code Left} value is present, false otherwise. + * + * @return true if left is present, false otherwise + */ + boolean isLeft(); + + /** + * Returns true if the {@code Right} value is present, false otherwise. + * + * @return true if right is present, false otherwise + */ + boolean isRight(); + + /** + * Run an effect if the {@code Left} value is present. Does nothing otherwise. + * + * @param effect a consumer function that receives the left value + * @return the same {@code Either} instance + */ + Either doOnLeft(Consumer effect); + + /** + * Run an effect if the {@code Right} value is present. Does nothing otherwise. + * + * @param effect effect a consumer function that receives the right value + * @return the same {@code Either} instance + */ + Either doOnRight(Consumer effect); + + /** + * Map the {@code Left} value to another if present. Does nothing otherwise. + * + * @param the type the left value will be mapped to + * @param mapper a function that receives the left value and returns another + * @return an {@code Either} instance with the mapped left value + */ + Either mapLeft(Function mapper); + + /** + * Map the {@code Right} value to another if present. Does nothing otherwise. + * + * @param the type the right value will be mapped to + * @param mapper a function that receives the right value and returns another + * @return an {@code Either} instance with the mapped right value + */ + Either mapRight(Function mapper); + + /** + * Terminal operator. Returns the {@code Left} value if present. Otherwise, + * it returns the provided fallback value. + * + * @param fallback the value to return if left is not present + * @return the left value or a fallback + */ + L leftOrElse(L fallback); + + /** + * Terminal operator. Returns the {@code Right} value if present. Otherwise, + * it returns the provided fallback value. + * + * @param fallback the value to return if right is not present + * @return the right value or a fallback + */ + R rightOrElse(R fallback); + + /** + * Terminal operator. Unwraps the {@code Either} to obtain the wrapped value. + * Since there's no possible way for the compiler to know which one is + * present ({@code Left} or {@code Right}), you need to provide a handler for + * both cases. Only the handler with the value present is used to unwrap and + * return the value. + * + * @param the type of the returned value + * @param onLeft a function to handle the left value if present + * @param onRight a function to handle the right value if present + * @return either the left or the right handled value + */ + T unwrap(Function onLeft, Function onRight); + + /** + * Terminal operator. Returns the {@code Left} value if present. Otherwise, + * it returns {@code null}. + * + * @return the left value or null + */ + default @Nullable L leftOrNull() { + return unwrap(Function.identity(), rigth -> null); + } + + /** + * Terminal operator. Returns the {@code Right} value if present. Otherwise, + * it returns {@code null}. + * + * @return the right value or null + */ + default @Nullable R rightOrNull() { + return unwrap(left -> null, Function.identity()); + } + + /** + * Terminal operator. Transforms the {@code Left} value to an {@link Optional}, + * which contains the value if present or is {@link Optional#empty()} otherwise. + * + * @return an {@code Optional } instance + */ + default Optional leftToOptional() { + return Optional.ofNullable(leftOrNull()); + } + + /** + * Terminal operator. Transforms the {@code Right} value to an {@link Optional}, + * which contains the value if present or is {@link Optional#empty()} otherwise. + * + * @return an {@code Optional } instance + */ + default Optional rightToOptional() { + return Optional.ofNullable(rightOrNull()); + } + + /** + * The {@code Left} implementation of {@link Either} + * + * @param the {@code Left} data type + * @param the {@code Right} data type + * @param value the left value + */ + record Left (L value) implements Either { + + /** + * Compact constructor to validate the value is not null. + * + * @param value the value of the instance + */ + public Left { + Objects.requireNonNull(value, "An Either cannot be created with a null value"); + } + + @Override + public boolean isLeft() { + return true; + } + + @Override + public boolean isRight() { + return false; + } + + @Override + public Either doOnLeft(final Consumer effect) { + effect.accept(this.value); + + return this; + } + + @Override + public Either doOnRight(final Consumer effect) { + return this; + } + + @Override + public Either mapLeft(final Function mapper) { + final var mappedLeft = mapper.apply(this.value); + + return new Left<>(mappedLeft); + } + + @Override + public Either mapRight(final Function mapper) { + return new Left<>(this.value); + } + + @Override + public L leftOrElse(final L fallback) { + return this.value; + } + + @Override + public R rightOrElse(final R fallback) { + return fallback; + } + + @Override + public T unwrap(final Function onLeft, final Function onRight) { + return onLeft.apply(this.value); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof final Left, ?> left) { + return this.value.equals(left.leftOrNull()); + } + + return false; + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return "Either[Left: %s]".formatted(this.value); + } + } + + /** + * The {@code Right} implementation of {@link Either} + * + * @param the {@code Left} data type + * @param the {@code Right} data type + * @param value the right value + */ + record Right (R value) implements Either { + + /** + * Compact constructor to validate the value is not null. + * + * @param value the value of the instance + */ + public Right { + Objects.requireNonNull(value, "An Either cannot be created with a null value"); + } + + @Override + public boolean isLeft() { + return false; + } + + @Override + public boolean isRight() { + return true; + } + + @Override + public Either doOnLeft(final Consumer effect) { + return this; + } + + @Override + public Either doOnRight(final Consumer effect) { + effect.accept(this.value); + + return this; + } + + @Override + public Either mapLeft(final Function mapper) { + return new Right<>(this.value); + } + + @Override + public Either mapRight(final Function mapper) { + final var mappedRight = mapper.apply(this.value); + + return new Right<>(mappedRight); + } + + @Override + public L leftOrElse(final L fallback) { + return fallback; + } + + @Override + public R rightOrElse(final R fallback) { + return this.value; + } + + @Override + public T unwrap(final Function onLeft, final Function onRight) { + return onRight.apply(this.value); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof final Right, ?> right) { + return this.value.equals(right.rightOrNull()); + } + + return false; + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public String toString() { + return "Either[Right: %s]".formatted(this.value); + } + } +} diff --git a/src/test/java/io/github/joselion/maybe/util/EitherTest.java b/src/test/java/io/github/joselion/maybe/util/EitherTest.java index 53f9e0d..da0262d 100644 --- a/src/test/java/io/github/joselion/maybe/util/EitherTest.java +++ b/src/test/java/io/github/joselion/maybe/util/EitherTest.java @@ -244,7 +244,7 @@ @Nested class when_the_left_value_is_present { @Test void returns_the_value_using_the_onLeft_handler() { final var onLeftSpy = Spy. >lambda("The value is: %s"::formatted); - final var onRightSpy = Spy. >lambda("The value is: %s"::formatted); + final var onRightSpy = Spy. >lambda(it -> "The value is: " + it); final var value = Either.ofLeft("foo").unwrap(onLeftSpy, onRightSpy); assertThat(value).isEqualTo("The value is: foo"); @@ -256,8 +256,8 @@ @Nested class when_the_right_value_is_present { @Test void returns_the_value_using_the_onRight_handler() { - final var onLeftSpy = Spy. >lambda("The value is: %s"::formatted); - final var onRightSpy = Spy. >lambda("The value is: %s"::formatted); + final var onLeftSpy = Spy. >lambda(it -> "The value is: " + it); + final var onRightSpy = Spy. >lambda(it -> "The value is: " + it); final var value = Either.ofRight("foo").unwrap(onLeftSpy, onRightSpy); assertThat(value).isEqualTo("The value is: foo");