-
-
Save chaotic3quilibrium/8b5116dbc2957cfea22032924935f10e to your computer and use it in GitHub Desktop.
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; | |
/** | |
* File: org.public_domain.java.utils.Either.java | |
* <p> | |
* Version: v2025.01.07 | |
* <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 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} | |
*/ | |
public static <L, R> Either<L, R> left(L value) { | |
return new Either<>(Optional.of(value), Optional.empty()); | |
} | |
/** | |
* 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} | |
*/ | |
public static <L, R> Either<L, R> right(R value) { | |
return new Either<>(Optional.empty(), Optional.of(value)); | |
} | |
/** | |
* 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} | |
*/ | |
public static <L, R> Either<L, R> from(Supplier<L> leftSupplier, 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} | |
*/ | |
public static <L extends Throwable, R> Either<L, R> tryCatch( | |
Supplier<R> successSupplier, | |
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} | |
*/ | |
public static <R> Either<RuntimeException, R> tryCatch(Supplier<R> successSupplier) { | |
return tryCatch(successSupplier, RuntimeException.class); | |
} | |
private final Optional<L> left; | |
private final Optional<R> right; | |
private Either(Optional<L> left, Optional<R> right) { | |
if (left.isEmpty() == right.isEmpty()) { | |
throw new IllegalArgumentException("left.isEmpty() must not be equal to right.isEmpty()"); | |
} | |
this.left = left; | |
this.right = right; | |
} | |
/** | |
* 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.left, that.left) | |
&& Objects.equals(this.right, that.right)); | |
} | |
/** | |
* Returns a hash code value for this instance. | |
* | |
* @return a hash code value for this instance | |
*/ | |
@Override | |
public int hashCode() { | |
return Objects.hash(this.left, this.right); | |
} | |
/** | |
* 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.left.isPresent(); | |
} | |
/** | |
* 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.right.isPresent(); | |
} | |
/** | |
* 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 | |
*/ | |
public L getLeft() { | |
return this.left.get(); | |
} | |
/** | |
* 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 | |
*/ | |
public R getRight() { | |
return this.right.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} | |
*/ | |
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} | |
*/ | |
public Optional<R> toOptionalRight() { | |
return this.right; | |
} | |
/** | |
* 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} | |
*/ | |
public Optional<L> toOptionalLeft() { | |
return this.left; | |
} | |
/** | |
* 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 | |
*/ | |
public <T> Either<L, T> map(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 | |
*/ | |
public <T> Either<L, T> flatMap( | |
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} | |
*/ | |
public Either<L, R> filterOrElse( | |
Predicate<R> retainingRight, | |
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} | |
*/ | |
public <T> Either<T, R> mapLeft(Function<? super L, ? extends T> leftFunction) { | |
return new Either<>( | |
this.left.map(l -> | |
Objects.requireNonNull(leftFunction.apply(l))), | |
this.right); | |
} | |
/** | |
* 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} | |
*/ | |
public <T> Either<L, T> mapRight(Function<? super R, ? extends T> rightFunction) { | |
return new Either<>( | |
this.left, | |
this.right.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} | |
*/ | |
public <T> Either<T, R> flatMapLeft( | |
Function<? super L, ? extends Either<? extends T, R>> leftFunction) { | |
return this.left | |
.<Either<T, R>>map(l -> | |
Either.left(Objects.requireNonNull(leftFunction.apply(l)).getLeft())) | |
.orElseGet(() -> | |
new Either<>( | |
Optional.empty(), | |
this.right)); | |
} | |
/** | |
* 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} | |
*/ | |
public <T> Either<L, T> flatMapRight( | |
Function<? super R, ? extends Either<L, ? extends T>> rightFunction) { | |
return this.right | |
.<Either<L, T>>map(r -> | |
Either.right(Objects.requireNonNull(rightFunction.apply(r)).getRight())) | |
.orElseGet(() -> | |
new Either<>( | |
this.left, | |
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} | |
*/ | |
public Either<L, R> filterOrElseLeft( | |
Predicate<L> retainingLeft, | |
Supplier<R> rightFunction | |
) { | |
return this.left | |
.<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} | |
*/ | |
public Either<L, R> filterOrElseRight( | |
Predicate<R> retainingRight, | |
Supplier<L> leftFunction | |
) { | |
return this.right | |
.<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 | |
*/ | |
public Either<R, L> swap() { | |
return new Either<>(this.right, this.left); | |
} | |
/** | |
* 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} | |
*/ | |
public <T> T converge( | |
Function<? super L, ? extends T> leftFunction, | |
Function<? super R, ? extends T> rightFunction | |
) { | |
return this.right | |
.<T>map(r -> | |
Objects.requireNonNull(rightFunction.apply(r))) | |
.orElseGet(() -> | |
this.left | |
.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 | |
*/ | |
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 | |
*/ | |
public static <T> T converge(Either<? extends T, ? extends T> either) { | |
return either.right.isPresent() | |
? either.right.get() | |
: either.left.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(Consumer<? super L> leftAction, Consumer<? super R> rightAction) { | |
this.right.ifPresent(rightAction); | |
this.left.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()); | |
} | |
} |
I think you have a bug in the flatMapLeft and flatMapRight methods. Specifically, if you do flatMapLeft on a Right or vice versa, you will get an exception.
Yep! You are correct.
That's what I deserve for not properly practicing TDD (Test Driven Development). A proper set of tests would have caught that. I am now working on that and hope to post it by the end of the holiday weekend.
I have now updated the code to v2023.11.23 with the changes.
I have now updated the Gist; updating the main file, and adding the TDD test file, both under the version "v2023.11.23a".
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>
.
Hi! I think you have a bug in the flatMapLeft and flatMapRight methods. Specifically, if you do flatMapLeft on a Right or vice versa, you will get an exception.
The reason is that java always evaluates function arguments before calling the function. So your 'orElse' call always creates its argument before actually invoking the method. That works if you flatMapLeft a Left or flatMapRight a right, but if you do the converse you attempt to create an Either that is empty on both sides.
The easy fix, it seems, is to lazy-evaluate by using .orElseGet() instead of .orElse().