Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save chaotic3quilibrium/8b5116dbc2957cfea22032924935f10e to your computer and use it in GitHub Desktop.
Save chaotic3quilibrium/8b5116dbc2957cfea22032924935f10e to your computer and use it in GitHub Desktop.
A Java class representing a disjoint-union, i.e. a value of one of two possible types (including reifying the try/catch statement)
package org.public_domain.java.utils;
import java.util.NoSuchElementException;
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;
import org.jetbrains.annotations.NotNull;
/**
* File: org.public_domain.java.utils.Either.java
* <p>
* Version: v2025.05.13
* <p>
* Represents a value of one of two possible types (a <a
* href="https://en.wikipedia.org/wiki/Disjoint_union">disjoint union</a>). An instance of
* {@link Either} is (constructor enforced via preconditions) to be well-defined and for whichever
* side is defined, the left or right, the value is guaranteed to be not-null.
* <p>
* -
* <p>
* A common use of {@link Either} is as an alternative to {@link Optional} for dealing with possibly
* erred or missing values. In this usage, {@link Optional#isEmpty} is replaced with
* {@link Either#getLeft} which, unlike {@link Optional#isEmpty}, can contain useful information,
* like a descriptive error message. {@link Either#getRight} takes the place of
* {@link java.util.Optional#get}
* <p>
* -
* <p>
* {@link Either} is right-biased, which means {@link Either#getRight} is assumed to be the default
* case upon which to operate. If it is defined for the left, operations like
* {@link Either#toOptional} returns {@link Optional#isEmpty}, and {@link Either#map} and
* {@link Either#flatMap} return the left value unchanged.
* <p>
* -
* <p>
* While inspired by the (first) solution presented in this <a
* href="https://stackoverflow.com/a/26164155/501113">StackOverflow Answer</a>, this updated version
* {@link Either} is internally implemented via a pair of {@link Optional}s, each of which
* explicitly reject null values by throwing a {@link java.lang.NullPointerException} from both
* factory methods, {@link Either#left} and {@link Either#right}.
**/
public final class Either<L, R> {
/**
* The right side of a disjoint union, as opposed to the left side.
*
* @param value instance of type R to be contained
* @param <L> the type of the left value to be contained
* @param <R> the type of the right value to be contained
* @return an instance of {@link Either} well-defined for the right side
* @throws NullPointerException if value is {@code null}
*/
@NotNull
public static <L, R> Either<L, R> right(@NotNull R value) {
return new Either<>(Optional.empty(), Optional.of(value));
}
/**
* The left side of a disjoint union, as opposed to the right side.
*
* @param value instance of type L to be contained
* @param <L> the type of the left value to be contained
* @param <R> the type of the right value to be contained
* @return an instance of {@link Either} well-defined for the left side
* @throws NullPointerException if value is {@code null}
*/
@NotNull
public static <L, R> Either<L, R> left(@NotNull L value) {
return new Either<>(Optional.of(value), Optional.empty());
}
/**
* Reify to an {@link Either}. If defined, place the {@link Optional} value into the right side of
* the {@link Either}, or else use the {@link Supplier} to define the left side of the
* {@link Either}.
*
* @param leftSupplier function invoked (only if rightOptional.isEmpty() returns true) to place
* the returned value for the left side of the {@link Either}
* @param rightOptional the contained value is placed into the right side of the {@link Either}
* @param <L> type of the instance provided by the {@link Supplier}
* @param <R> type of the value in the instance of the {@link Optional}
* @return a well-defined instance of {@link Either}
* @throws NullPointerException if leftSupplier, the value returned if called, rightOptional, or
* the value returned if extracted, is {@code null}
*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@NotNull
public static <L, R> Either<L, R> from(
@NotNull Supplier<L> leftSupplier,
@NotNull Optional<R> rightOptional
) {
return rightOptional
.map(Either::<L, R>right)
.orElseGet(() -> Either.left(Objects.requireNonNull(leftSupplier.get())));
}
/**
* Reify a try/catch statement into an {@link Either}. When the {@link Supplier} is invoked, if
* there is no {@link Throwable} exception thrown, the value returned by the {@link Supplier} is
* returned within the right side of an {@link Either}. If an {@link Throwable} exception is
* thrown and the exception is an instance of the {@code throwableType}, the exception is returned
* within the left side of an {@link Either}. Otherwise, the exception is re-thrown.
*
* @param successSupplier function wrapped in the {@code try {...} catch (Throwable ...) {...} }
* block, that when invoked, if there is no exception thrown, the function's return value is
* returned within the right side of an {@link Either}
* @param throwableType if the {@code successSupplier} function throws an exception, if it is an
* instance of {@code throwableType}, the exception is returned within the left side of an
* {@link Either}, otherwise, the exception is re-{@code thrown}
* @param <L> type of the {@link Throwable} instance being caught
* @param <R> type of the instance provided by the {@link Supplier}
* @return a well-defined instance of {@link Either}
*/
@SuppressWarnings("unchecked")
@NotNull
public static <L extends Throwable, R> Either<L, R> tryCatch(
@NotNull Supplier<R> successSupplier,
@NotNull Class<L> throwableType
) {
try {
return Either.right(Objects.requireNonNull(successSupplier.get()));
} catch (Throwable throwable) {
if (throwableType.isInstance(throwable)) {
return Either.left((L) throwable);
}
throw throwable;
}
}
/**
* Reify a try/catch statement into an {@link Either}. If when the {@link Supplier} is invoked
* there is no {@link Throwable} exception thrown, the value returned by the {@link Supplier} is
* returned within the right side of an {@link Either}. If an {@link RuntimeException} exception
* is thrown, the exception is returned within the left side of an {@link Either}, otherwise the
* exception is re-thrown.
*
* @param successSupplier function wrapped in the {@code try {...} catch (Throwable ...) {...} }
* block, and when invoked, if there is no exception thrown, the function's return value is
* returned within the right side of an {@link Either}
* @param <R> type of the instance provided by the {@link Supplier}
* @return a well-defined instance of {@link Either}
*/
@NotNull
public static <R> Either<RuntimeException, R> tryCatch(@NotNull Supplier<R> successSupplier) {
return tryCatch(successSupplier, RuntimeException.class);
}
private final boolean isRight;
private final Object defined;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private Either(@NotNull Optional<L> left, @NotNull Optional<R> right) {
if (left.isEmpty() == right.isEmpty()) {
throw new IllegalArgumentException("left.isEmpty() must not be equal to right.isEmpty()");
}
this.isRight = right.isPresent();
this.defined = isRight
? right
: left;
}
/**
* Indicates whether some other instance is equivalent to {@code this}.
*
* @param object reference instance with which to compare
* @return true if {@code this} instance is the equivalent value as the object argument
*/
@Override
public boolean equals(Object object) {
return (this == object) ||
((object instanceof Either<?, ?> that)
&& Objects.equals(this.isRight, that.isRight)
&& Objects.equals(this.defined, that.defined));
}
/**
* Returns a hash code value for this instance.
*
* @return a hash code value for this instance
*/
@Override
public int hashCode() {
return Objects.hash(this.isRight, this.defined);
}
/**
* Returns true if this {@link Either} is defined on the right side
*
* @return true if the right side of this {@link Either} contains a value
*/
public boolean isRight() {
return this.isRight;
}
/**
* Returns true if this {@link Either} is defined on the left side
*
* @return true if the left side of this {@link Either} contains a value
*/
public boolean isLeft() {
return !this.isRight;
}
/**
* If defined (which can be detected with {@link Either#isRight}), returns the value for the right
* side of {@link Either}, or else throws an {@link NoSuchElementException}
*
* @return value of type R for the left, if the right side of this {@link Either} is defined
* @throws NoSuchElementException if the right side of this {@link Either} is not defined
*/
@SuppressWarnings("OptionalGetWithoutIsPresent")
@NotNull
public R getRight() {
return toOptionalRight().get();
}
/**
* If defined (which can be detected with {@link Either#isLeft}), returns the value for the left
* side of {@link Either}, or else throws an {@link NoSuchElementException}.
*
* @return value of type L for the left, if the left side of this {@link Either} is defined
* @throws NoSuchElementException if the left side of this {@link Either} is not defined
*/
@SuppressWarnings("OptionalGetWithoutIsPresent")
@NotNull
public L getLeft() {
return toOptionalLeft().get();
}
/**
* Reduce to an Optional. If defined, returns the value for the right side of {@link Either} in an
* {@link Optional#of}, or else returns {@link Optional#empty}. Forwards call to
* {@link Either#toOptionalRight}.
*
* @return an {@link Optional} containing the right side if defined, or else returns
* {@link Optional#empty}
*/
@NotNull
public Optional<R> toOptional() {
return toOptionalRight();
}
/**
* Reduce to an Optional. If defined, returns the value for the right side of {@link Either} in an
* {@link Optional#of}, or else returns {@link Optional#empty}.
*
* @return an {@link Optional} containing the right side if defined, or else returns
* {@link Optional#empty}
*/
@SuppressWarnings("unchecked")
@NotNull
public Optional<R> toOptionalRight() {
return this.isRight()
? ((Optional<R>) this.defined)
: Optional.empty();
}
/**
* Reduce to an Optional. If defined, returns the value for the left side of {@link Either} in an
* {@link Optional#of}, or else returns {@link Optional#empty}.
*
* @return an {@link Optional} containing the left side if defined, or else returns
* {@link Optional#empty}
*/
@SuppressWarnings("unchecked")
@NotNull
public Optional<L> toOptionalLeft() {
return !this.isRight()
? ((Optional<L>) this.defined)
: Optional.empty();
}
/**
* If right is defined, the given map translation function is applied. Forwards call to
* {@link Either#mapRight}.
*
* @param rightFunction given function which is only applied if right is defined
* @param <T> target type to which R is translated
* @return result of the function translation, replacing type R with type T
*/
@NotNull
public <T> Either<L, T> map(@NotNull Function<? super R, ? extends T> rightFunction) {
return mapRight(rightFunction);
}
/**
* If right is defined, the given flatMap translation function is applied. Forwards call to
* {@link Either#flatMapRight}.
*
* @param rightFunction given function which is only applied if right is defined
* @param <T> target type to which R is translated
* @return result of the function translation, replacing type R with type T
*/
@NotNull
public <T> Either<L, T> flatMap(
@NotNull Function<? super R, ? extends Either<L, ? extends T>> rightFunction
) {
return flatMapRight(rightFunction);
}
/**
* Returns {@link Either#left} when {@link Either#isLeft} is {@code true}, or returns
* {@link Either#right} when {@link Either#isRight} is {@code true} and {@code retainingRight}
* returns {@code true}, or returns the value returned by {@code leftFunction} within
* {@link Either#left}. Forwards call to {@link Either#filterOrElseRight}
*
* @param retainingRight predicate which is only applied if {@link Either#isRight} is
* {@code true}
* @param leftFunction given function which is only applied if {@link Either#isRight} is
* {@code true} and {@code retainingRight} returns {@code false}
* @return {@link Either#left} when {@link Either#isLeft} is {@code true}, or returns
* {@link Either#right} when {@link Either#isRight} is {@code true} and {@code retainingRight}
* returns {@code true}
*/
@NotNull
public Either<L, R> filterOrElse(
@NotNull Predicate<R> retainingRight,
@NotNull Supplier<L> leftFunction
) {
return filterOrElseRight(retainingRight, leftFunction);
}
/**
* If left is defined, the given map translation function is applied.
*
* @param leftFunction given function which is only applied if left is defined
* @param <T> target type to which L is translated
* @return result of the function translation, replacing type L with type T
* @throws NullPointerException if leftFunction or the value it returns is {@code null}
*/
@NotNull
public <T> Either<T, R> mapLeft(@NotNull Function<? super L, ? extends T> leftFunction) {
return new Either<>(
this.toOptionalLeft().map(l ->
Objects.requireNonNull(leftFunction.apply(l))),
this.toOptionalRight());
}
/**
* If right is defined, the given map translation function is applied.
*
* @param rightFunction given function which is only applied if right is defined
* @param <T> target type to which R is translated
* @return result of the function translation, replacing type R with type T
* @throws NullPointerException if rightFunction or the value it returns is {@code null}
*/
@NotNull
public <T> Either<L, T> mapRight(@NotNull Function<? super R, ? extends T> rightFunction) {
return new Either<>(
this.toOptionalLeft(),
this.toOptionalRight().map(r ->
Objects.requireNonNull(rightFunction.apply(r))));
}
/**
* If left is defined, the given flatMap translation function is applied.
*
* @param leftFunction given function which is only applied if left is defined
* @param <T> target type to which L is translated
* @return result of the function translation, replacing type L with type T
* @throws NullPointerException if leftFunction or the value it returns is {@code null}
*/
@NotNull
public <T> Either<T, R> flatMapLeft(
@NotNull Function<? super L, ? extends Either<? extends T, R>> leftFunction
) {
return this.toOptionalLeft()
.<Either<T, R>>map(l ->
Either.left(Objects.requireNonNull(leftFunction.apply(l)).getLeft()))
.orElseGet(() ->
new Either<>(
Optional.empty(),
this.toOptionalRight()));
}
/**
* If right is defined, the given flatMap translation function is applied.
*
* @param rightFunction given function which is only applied if right is defined
* @param <T> target type to which R is translated
* @return result of the function translation, replacing type R with type T
* @throws NullPointerException if rightFunction or the value it returns is {@code null}
*/
@NotNull
public <T> Either<L, T> flatMapRight(
@NotNull Function<? super R, ? extends Either<L, ? extends T>> rightFunction
) {
return this.toOptionalRight()
.<Either<L, T>>map(r ->
Either.right(Objects.requireNonNull(rightFunction.apply(r)).getRight()))
.orElseGet(() ->
new Either<>(
this.toOptionalLeft(),
Optional.empty()));
}
/**
* Returns {@link Either#right} when {@link Either#isRight} is {@code true}, or returns
* {@link Either#left} when {@link Either#isLeft} is {@code true} and {@code retainingLeft}
* returns {@code true}, or returns the value returned by {@code rightFunction} within
* {@link Either#right}.
*
* @param retainingLeft predicate which is only applied if {@link Either#isLeft} is {@code true}
* @param rightFunction given function which is only applied if {@link Either#isLeft} is
* {@code true} and {@code retainingLeft} returns {@code false}
* @return {@link Either#right} when {@link Either#isRight} is {@code true}, or returns
* {@link Either#left} when {@link Either#isLeft} is {@code true} and {@code retainingLeft}
* returns {@code true}, or returns the value returned by {@code rightFunction} within
* {@link Either#right}
*/
@NotNull
public Either<L, R> filterOrElseLeft(
@NotNull Predicate<L> retainingLeft,
@NotNull Supplier<R> rightFunction
) {
return this.toOptionalLeft()
.<Either<L, R>>map(l ->
retainingLeft.test(l)
? this
: Either.right(rightFunction.get()))
.orElse(this);
}
/**
* Returns {@link Either#left} when {@link Either#isLeft} is {@code true}, or returns
* {@link Either#right} when {@link Either#isRight} is {@code true} and {@code retainingRight}
* returns {@code true}, or returns the value returned by {@code leftFunction} within
* {@link Either#left}.
*
* @param retainingRight predicate which is only applied if {@link Either#isRight} is
* {@code true}
* @param leftFunction given function which is only applied if {@link Either#isRight} is
* {@code true} and {@code retainingRight} returns {@code false}
* @return {@link Either#left} when {@link Either#isLeft} is {@code true}, or returns
* {@link Either#right} when {@link Either#isRight} is {@code true} and {@code retainingRight}
* returns {@code true}
*/
@NotNull
public Either<L, R> filterOrElseRight(
@NotNull Predicate<R> retainingRight,
@NotNull Supplier<L> leftFunction
) {
return this.toOptionalRight()
.<Either<L, R>>map(r ->
retainingRight.test(r)
? this
: Either.left(leftFunction.get()))
.orElse(this);
}
/**
* Returns reversed type such that if this is a {@link Either#left}, then return the
* {@link Either#left} value in {@link Either#right} or vice versa.
*
* @return reversed type such that if this is a {@link Either#left}, then return the
* {@link Either#left} value in {@link Either#right} or vice versa
*/
@NotNull
public Either<R, L> swap() {
return new Either<>(this.toOptionalRight(), this.toOptionalLeft());
}
/**
* Converge the distinct types, L and R, to a common type, T. This method's implementation is
* right-biased.
*
* @param leftFunction given function which is only applied if left is defined
* @param rightFunction given function which is only applied if right is defined
* @param <T> type of the returned instance
* @return an instance of T
* @throws NullPointerException if leftFunction, the value it returns, rightFunction, or the value
* it returns is {@code null}
*/
@NotNull
public <T> T converge(
@NotNull Function<? super L, ? extends T> leftFunction,
@NotNull Function<? super R, ? extends T> rightFunction
) {
return this.toOptionalRight()
.<T>map(r ->
Objects.requireNonNull(rightFunction.apply(r)))
.orElseGet(() ->
this.toOptionalLeft()
.map(l ->
Objects.requireNonNull(leftFunction.apply(l)))
.orElseThrow(() ->
new IllegalStateException("should never get here")));
}
/**
* Converge the distinct types, L and R, to a common type, T. This method's implementation is
* right-biased. It is the equivalent of the following method call:
* <p>
* {@code this.converge(Function.identity(), Function.identity())}
* <p>
* ---
* <p>
* **WARNING:**:
* <p>
* The validity of type T is only checked at run time, not at compile time. This is due to an
* issue with Java generics.
* <p>
* My preferred method of solving this would have been...
* <pre>
* {@code public <T extends L & R> T converge() {
* return left.isPresent()
* ? left.get()
* : right.get();
* }}
* </pre>
* However, this produces a compiler error on the R in {@code <T extends L & R> }, as explained
* <a href="https://stackoverflow.com/a/30829160/501113">here</a>.
*
* @param <T> type of the returned instance
* @return an instance of T
*/
@SuppressWarnings("unchecked")
@NotNull
public <T> T converge() {
return (T) converge(this);
}
/**
* Converge the distinct types, L and R, to a common type, T. This method's implementation is
* right-biased.
* <p>
* {@code var t = either.converge(Function.identity(), Function.identity())}
* <p>
* ---
* <p>
* Note: The validity of type T is checked at compile time.
*
* @param either the instance of {@link Either} where both L and R share T as a common supertype
* @param <T> type of the returned instance
* @return an instance of T
*/
@SuppressWarnings("OptionalGetWithoutIsPresent")
@NotNull
public static <T> T converge(@NotNull Either<? extends T, ? extends T> either) {
return either.toOptionalRight().isPresent()
? either.toOptionalRight().get()
: either.toOptionalLeft().get();
}
/**
* Execute the given side-effecting function depending upon which side is defined. This method's
* implementation is right-biased.
*
* @param leftAction given function is only executed if left is defined
* @param rightAction given function is only executed if right is defined
*/
public void forEach(
@NotNull Consumer<? super L> leftAction,
@NotNull Consumer<? super R> rightAction
) {
this.toOptionalRight().ifPresent(rightAction);
this.toOptionalLeft().ifPresent(leftAction);
}
}
package org.public_domain.java.utils;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
/**
* File: org.public_domain.java.utils.EitherTests.java
* <p>
* Version: v2025.01.07
*
* <p>
**/
public class EitherTests {
private void validateLeft(Integer leftValue, Either<Integer, String> eitherLeft) {
assertTrue(eitherLeft.isLeft());
assertFalse(eitherLeft.isRight());
assertEquals(leftValue, eitherLeft.getLeft());
var noSuchElementExceptionLeft =
assertThrows(
NoSuchElementException.class,
eitherLeft::getRight);
assertEquals(
"No value present",
noSuchElementExceptionLeft.getMessage());
var nullRightNullPointerException =
assertThrows(
NullPointerException.class,
() -> Either.right(null));
assertNull(nullRightNullPointerException.getMessage());
}
private void validateRight(String rightValue, Either<Integer, String> eitherRight) {
assertFalse(eitherRight.isLeft());
assertTrue(eitherRight.isRight());
assertEquals(rightValue, eitherRight.getRight());
var noSuchElementExceptionLeft =
assertThrows(
NoSuchElementException.class,
eitherRight::getLeft);
assertEquals(
"No value present",
noSuchElementExceptionLeft.getMessage());
var nullLefttNullPointerException =
assertThrows(
NullPointerException.class,
() -> Either.right(null));
assertNull(nullLefttNullPointerException.getMessage());
}
//The testFactory* tests also test via the validate* methods:
// - isLeft()
// - isRight()
// - getLeft()
// - getRight()
@Test
public void testFactoryLeft() {
var nullLeftNullPointerException =
assertThrows(
NullPointerException.class,
() -> Either.left(null));
assertNull(nullLeftNullPointerException.getMessage());
Either<Integer, String> eitherLeft = Either.left(10);
validateLeft(10, eitherLeft);
}
@Test
public void testFactoryRight() {
var nullRightNullPointerException =
assertThrows(
NullPointerException.class,
() -> Either.right(null));
assertNull(nullRightNullPointerException.getMessage());
Either<Integer, String> eitherRight = Either.right("Eleven");
validateRight("Eleven", eitherRight);
}
@Test
public void testFactoryFromLeft() {
Either<Integer, String> eitherFromLeft = Either.from(() -> 30, Optional.empty());
validateLeft(30, eitherFromLeft);
var nullLeftNullPointerException =
assertThrows(
NullPointerException.class,
() -> Either.from(() -> (String) null, Optional.empty()));
assertNull(nullLeftNullPointerException.getMessage());
}
@Test
public void testFactoryFromRight() {
Either<Integer, String> eitherFromRight = Either.from(() -> 32, Optional.of("ThirtyOne"));
validateRight("ThirtyOne", eitherFromRight);
}
@Test
public void testToOptional() {
Either<Integer, String> eitherFromLeft = Either.from(() -> 40, Optional.empty());
assertTrue(eitherFromLeft.toOptional().isEmpty());
Either<Integer, String> eitherFromRight = Either.from(() -> 42, Optional.of("FortyOne"));
assertTrue(eitherFromRight.toOptional().isPresent());
assertEquals("FortyOne", eitherFromRight.toOptional().get());
}
//Since map/flatMap are both forwarded to mapRight/flatMapRight, skipping writing redundant tests
@Test
public void testMapLeft() {
Either<Integer, String> eitherLeft = Either.left(30);
var eitherLeftTransformed = eitherLeft.mapLeft(Object::toString);
assertTrue(eitherLeftTransformed.isLeft());
assertFalse(eitherLeftTransformed.isRight());
assertEquals("30", eitherLeftTransformed.getLeft());
var nullReturnValuePointerException =
assertThrows(
NullPointerException.class,
() -> eitherLeftTransformed.mapLeft(integer -> (String) null));
assertNull(nullReturnValuePointerException.getMessage());
//mapRight should return an equivalent instance
assertEquals(eitherLeftTransformed, eitherLeftTransformed.mapRight(string -> "should never get here"));
}
@Test
public void testMapRight() {
Either<Integer, String> eitherRight = Either.right("31");
var eitherRightTransformed = eitherRight.mapRight(Integer::parseInt);
assertFalse(eitherRightTransformed.isLeft());
assertTrue(eitherRightTransformed.isRight());
assertEquals(31, eitherRightTransformed.getRight());
var nullReturnValuePointerException =
assertThrows(
NullPointerException.class,
() -> eitherRightTransformed.mapRight(string -> (Integer) null));
assertNull(nullReturnValuePointerException.getMessage());
//mapLeft should return an equivalent instance
assertEquals(eitherRightTransformed, eitherRightTransformed.mapLeft(integer -> Integer.MAX_VALUE)); //should never get to the lambda
}
@Test
public void testFlatMapLeft() {
Either<Integer, String> eitherLeft = Either.left(40);
var eitherLeftTransformed = eitherLeft.flatMapLeft(l -> Either.left(l.toString()));
assertTrue(eitherLeftTransformed.isLeft());
assertFalse(eitherLeftTransformed.isRight());
assertEquals("40", eitherLeftTransformed.getLeft());
var nullReturnValuePointerException =
assertThrows(
NullPointerException.class,
() -> eitherLeftTransformed.mapLeft(integer -> (String) null));
assertNull(nullReturnValuePointerException.getMessage());
//mapRight should return an equivalent instance
assertEquals(eitherLeftTransformed, eitherLeftTransformed.mapRight(string -> "should never get here"));
}
@Test
public void testFlatMapRight() {
Either<Integer, String> eitherRight = Either.right("41");
var eitherRightTransformed = eitherRight.flatMapRight(r -> Either.right(Integer.parseInt(r)));
assertFalse(eitherRightTransformed.isLeft());
assertTrue(eitherRightTransformed.isRight());
assertEquals(41, eitherRightTransformed.getRight());
var nullReturnValuePointerException =
assertThrows(
NullPointerException.class,
() -> eitherRightTransformed.mapRight(string -> (Integer) null));
assertNull(nullReturnValuePointerException.getMessage());
//mapLeft should return an equivalent instance
assertEquals(eitherRightTransformed, eitherRightTransformed.mapLeft(integer -> Integer.MAX_VALUE)); //should never get to the lambda
}
@Test
public void testConvergeFunctions() {
Either<Integer, Double> eitherLeft = Either.left(50);
var convergedLeft = eitherLeft.converge(Object::toString, Object::toString);
assertEquals("50", convergedLeft);
Either<Integer, Double> eitherRight = Either.right(51.0d);
var convergedRight = eitherRight.converge(Object::toString, Object::toString);
assertEquals("51.0", convergedRight);
}
@Test
public void testConvergeIdentityInstance() {
Either<Integer, Double> eitherLeft = Either.left(50);
var convergedLeft = eitherLeft.<Number>converge();
assertEquals(50, convergedLeft);
Either<Integer, Double> eitherRight = Either.right(51.0d);
var convergedRight = eitherRight.<Number>converge();
assertEquals(51.0d, convergedRight);
}
@Test
public void testConvergeIdentityStatic() {
Either<Integer, Double> eitherLeft = Either.left(50);
var convergedLeft = Either.converge(eitherLeft);
assertEquals(50, convergedLeft);
Either<Integer, Double> eitherRight = Either.right(51.0d);
var convergedRight = Either.converge(eitherRight);
assertEquals(51.0d, convergedRight);
}
@Test
public void testForEach() {
Either<Integer, String> eitherLeft = Either.left(60);
var leftLr = new boolean[2];
eitherLeft.forEach(
l -> leftLr[0] = true,
r -> leftLr[1] = true);
assertArrayEquals(new boolean[]{true, false}, leftLr);
Either<Integer, String> eitherRight = Either.right("SixtyOne");
var rightLr = new boolean[2];
eitherRight.forEach(
l -> rightLr[0] = true,
r -> rightLr[1] = true);
assertArrayEquals(new boolean[]{false, true}, rightLr);
}
@Test
public void testTryCatchRuntimeException() {
var eitherLeft = Either.tryCatch(() -> 60 / 0);
assertTrue(eitherLeft.isLeft());
assertEquals(ArithmeticException.class, eitherLeft.getLeft().getClass());
assertEquals("/ by zero", eitherLeft.getLeft().getMessage());
var eitherRight = Either.tryCatch(() -> 60 / 2);
assertTrue(eitherRight.isRight());
assertEquals(30, eitherRight.getRight());
}
private static Stream<Arguments> provideTestTryCatchThrowable() {
return Stream.of(
Arguments.of(Throwable.class),
Arguments.of(Exception.class),
Arguments.of(RuntimeException.class),
Arguments.of(ArithmeticException.class));
}
@ParameterizedTest
@MethodSource("provideTestTryCatchThrowable")
public void testTryCatchThrowable(
Class<Throwable> exceptionTypeProvided
) {
var eitherLeft = Either.tryCatch(() -> 60 / 0, exceptionTypeProvided);
assertTrue(eitherLeft.isLeft());
assertEquals(ArithmeticException.class, eitherLeft.getLeft().getClass());
assertEquals("/ by zero", eitherLeft.getLeft().getMessage());
}
@Test
public void testTryCatchThrowableRethrow() {
var message = "testTryCatchThrowableRethrow message";
var throwable = assertThrows(
IllegalStateException.class,
() -> Either.tryCatch(() -> {
throw new IllegalStateException(message);
},
ArithmeticException.class));
assertEquals(message, throwable.getMessage());
}
@Test
public void testFilterOrElse() {
var integer = 162;
var string = "OneSixtyThree";
Either<Integer, String> eitherLeft = Either.left(integer);
Either<Integer, String> eitherRight = Either.right(string);
var eitherRight2 = eitherRight.filterOrElse(r -> Objects.equals(r, string), () -> integer);
assertEquals(eitherRight, eitherRight2);
var eitherRight3 = eitherRight.filterOrElse(r -> !Objects.equals(r, string), () -> integer);
assertEquals(eitherLeft, eitherRight3);
var eitherRight4 = eitherLeft.filterOrElse(r -> Objects.equals(r, string), () -> integer);
assertEquals(eitherLeft, eitherRight4);
}
@Test
public void testFilterOrElseLeft() {
var integer = 60;
var string = "SixtyOne";
Either<Integer, String> eitherLeft = Either.left(integer);
Either<Integer, String> eitherRight = Either.right(string);
var eitherLeft2 = eitherLeft.filterOrElseLeft(l -> Objects.equals(l, integer), () -> string);
assertEquals(eitherLeft, eitherLeft2);
var eitherLeft3 = eitherLeft.filterOrElseLeft(l -> !Objects.equals(l, integer), () -> string);
assertEquals(eitherRight, eitherLeft3);
var eitherLeft4 = eitherRight.filterOrElseLeft(l -> Objects.equals(l, integer), () -> string);
assertEquals(eitherRight, eitherLeft4);
}
@Test
public void testFilterOrElseRight() {
var integer = 62;
var string = "SixtyThree";
Either<Integer, String> eitherLeft = Either.left(integer);
Either<Integer, String> eitherRight = Either.right(string);
var eitherRight2 = eitherRight.filterOrElseRight(r -> Objects.equals(r, string), () -> integer);
assertEquals(eitherRight, eitherRight2);
var eitherRight3 = eitherRight.filterOrElseRight(r -> !Objects.equals(r, string), () -> integer);
assertEquals(eitherLeft, eitherRight3);
var eitherRight4 = eitherLeft.filterOrElseRight(r -> Objects.equals(r, string), () -> integer);
assertEquals(eitherLeft, eitherRight4);
}
@Test
public void testSwap() {
var integer = 60;
Either<Integer, String> eitherLeft = Either.left(integer);
assertEquals(integer, eitherLeft.getLeft());
var eitherRight = eitherLeft.swap();
assertEquals(integer, eitherRight.getRight());
var eitherLeft2 = eitherRight.swap();
assertEquals(integer, eitherLeft2.getLeft());
}
}
@chaotic3quilibrium
Copy link
Author

I have now updated the Gist; updating the main file, and adding the TDD test file, both under the version "v2023.11.23a".

@chaotic3quilibrium
Copy link
Author

chaotic3quilibrium commented Dec 23, 2024

I have updated the code in both files to incorporate the specialization of Either<L extends Throwable, R>, which is returned by the two new methods, tryCatch().

These two methods were a much simpler way of achieving the goal, encapsulation of a try {...} catch (Throwable throwable) {...} statement, than essentially duplicating almost all of Either<L, R> into something similar to Scala's Try<T>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment