Skip to content

Instantly share code, notes, and snippets.

@chaotic3quilibrium
Last active November 23, 2023 20:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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 value of one of two possible types
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.Supplier;
/**
* File: org.public_domain.java.utils.Either.java
* <p>
* Version: v2023.11.23a
* <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 Objects.requireNonNull(rightOptional)
.map(r -> Either.<L, R>right(Objects.requireNonNull(r)))
.orElseGet(() -> Either.left(leftSupplier.get()));
}
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}.
*
* @return an {@link Optional} containing the right side if defined, or else returns {@link Optional#empty}
*/
public Optional<R> toOptional() {
return this.right;
}
/**
* 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);
}
/**
* 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(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(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(Objects.requireNonNull(rightFunction).apply(r)).getRight()))
.orElseGet(() ->
new Either<>(
this.left,
Optional.empty()));
}
/**
* 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(Objects.requireNonNull(rightFunction).apply(r)))
.orElseGet(() ->
this.left
.map(l ->
Objects.requireNonNull(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 org.junit.jupiter.api.Test;
import java.util.NoSuchElementException;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
/**
* File: org.public_domain.java.utils.EitherTests.java
* <p>
* Version: v2023.11.23a
* <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 testFactoryFromNulls() {
var nullNullFromNullPointerException =
assertThrows(
NullPointerException.class,
() -> Either.<Integer, String>from(null, null));
assertNull(nullNullFromNullPointerException.getMessage());
var definedNullFromNullPointerException =
assertThrows(
NullPointerException.class,
() -> Either.<Integer, String>from(() -> 20, null));
assertNull(definedNullFromNullPointerException.getMessage());
var nullDefinedFromNullPointerException =
assertThrows(
NullPointerException.class,
() -> Either.<Integer, String>from(null, Optional.empty()));
assertEquals("""
Cannot invoke "java.util.function.Supplier.get()" because "leftSupplier" is null""",
nullDefinedFromNullPointerException.getMessage());
Either<Integer, String> eitherFromRight = Either.from(null, Optional.of("TwentyOne"));
validateRight("TwentyOne", eitherFromRight);
}
@Test
public void testFactoryFromLeft() {
Either<Integer, String> eitherFromLeft = Either.from(() -> 30, Optional.empty());
validateLeft(30, eitherFromLeft);
}
@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 nullLambdaNullPointerException =
assertThrows(
NullPointerException.class,
() -> eitherLeftTransformed.mapLeft(null));
assertNull(nullLambdaNullPointerException.getMessage());
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 nullLambdaNullPointerException =
assertThrows(
NullPointerException.class,
() -> eitherRightTransformed.mapRight(null));
assertNull(nullLambdaNullPointerException.getMessage());
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 nullLambdaNullPointerException =
assertThrows(
NullPointerException.class,
() -> eitherLeftTransformed.mapLeft(null));
assertNull(nullLambdaNullPointerException.getMessage());
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 nullLambdaNullPointerException =
assertThrows(
NullPointerException.class,
() -> eitherRightTransformed.mapRight(null));
assertNull(nullLambdaNullPointerException.getMessage());
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);
}
}
@roofpig95008
Copy link

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().

@chaotic3quilibrium
Copy link
Author

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.

@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".

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