Either - Turn Your Partial Function into a Total Function
Why Either
Either
can turn a partial function into a total function.
When your function (or method) is a partial function meaning that it may not have a return value for some inputs, your code becomes hard to understand and maintain because it may throw an Exception
when there's no corresponding result for a such input. Throwing an Exception
means that there might be any number exceptions that can be thrown from the function, and to see what might be thrown, you have to check the implementation details.
To make it a total function so that it always has the output for the given input, there can be multiple ways but an easy and recommended way is using Either
.
Either
is right-biased meaning that the most well-known combinators on Either
(e.g. map
, flatMap
, etc.) are by default for the Right
value. If you want to manipulate the left one, you need to use the combinators for Left
which are usually prefixed with left
(e.g. leftMap
, leftFlatMap
, etc.).
Create
Either.right to contain the result
import j8plus.types.Either;
Either.<String, Integer>right(1);
// Either<String, Integer> = Right(1)
Either.right to contain the error
import j8plus.types.Either;
Either.<String, Integer>left("Error message");
// Either<String, Integer> = Left("Error message")
Either
from Optional
import j8plus.types.Either;
final Optional<Integer> num = Optional.ofNullable(1);
Either.fromOptional(num, () -> "No number found");
// Either<String, Integer> = Right(1)
final Optional<Integer> noNum = Optional.empty();
Either.fromOptional(noNum, () -> "No number found");
// Either<String, Integer> = Left("No number found")
Either
from Maybe
import j8plus.types.Maybe;
import j8plus.types.Either;
final Maybe<Integer> num = Maybe.maybe(1);
Either.fromMaybe(num, () -> "No number found");
// Either<String, Integer> = Right(1)
final Maybe<Integer> noNum = Maybe.nothing();
Either.fromMaybe(noNum, () -> "No number found");
// Either<String, Integer> = Left("No number found")
Transform
Either.map
import j8plus.types.Either;
final Either<String, Integer> errorOrNumber = Either.right(5);
// Either<String, Integer> = Right(5)
errorOrNumber.map(n -> n * 2);
// Either<String, Integer> = Right(10)
Either.flatMap
import j8plus.types.Either;
public Either<String, Integer> foo(int n) {
if (n < 0) {
return Either.left(
"foo can't take a negative int. [n: " + n + "]"
);
} else {
return Either.right(n * 2);
}
}
public Either<String, Integer> bar(int n) {
if (n < 100) {
return Either.left(
"bar can't take an int less than 100. [n: " + n + "]"
);
} else {
return Either.right(n - 100);
}
}
All happy paths
Either.<String, Integer>right(50) // Either<String, Integer> = Right(50)
.flatMap(n -> foo(n)) // Either<String, Integer> = Right(100)
.flatMap(n -> bar(n)) // Either<String, Integer> = Right(0)
// Or with method references
Either.<String, Integer>right(50) // Either<String, Integer> = Right(50)
.flatMap(this::foo) // Either<String, Integer> = Right(100)
.flatMap(this::bar) // Either<String, Integer> = Right(0)
Unhappy path cases
Either.<String, Integer>right(-1) // Either<String, Integer> = Right(-1)
.flatMap(n -> foo(n)) // Either<String, Integer> = Left("foo can't take a negative int. [n: -1]")
.flatMap(n -> bar(n)); // Either<String, Integer> = Left("foo can't take a negative int. [n: -1]")
// Or with method references
Either.<String, Integer>right(-1) // Either<String, Integer> = Right(-1)
.flatMap(this::foo) // Either<String, Integer> = Left("foo can't take a negative int. [n: -1]")
.flatMap(this::bar); // Either<String, Integer> = Left("foo can't take a negative int. [n: -1]")
Either.<String, Integer>right(5) // Either<String, Integer> = Right(5)
.flatMap(n -> foo(n)) // Either<String, Integer> = Right(10)
.flatMap(n -> bar(n)); // Either<String, Integer> = Left("bar can't take an int less than 100. [n: 10]")
// Or with method references
Either.<String, Integer>right(5) // Either<String, Integer> = Right(5)
.flatMap(this::foo) // Either<String, Integer> = Right(10)
.flatMap(this::bar); // Either<String, Integer> = Left("bar can't take an int less than 100. [n: 10]")
Either.leftMap
If you want to change the left value, you can use map
to do it.
import j8plus.types.Either;
public class MyError {
public final String message;
public MyError(final String message) {
this.message = message;
}
@Override
public String toString() {
return "MyError(message=" + message + ")";
}
}
final Either<String, Integer> errorMsgOrNum = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum.leftMap(err -> new MyError(err));
// Either<MyError, Integer> = Left(MyError(message=Error message))
Either.leftFlatMap
Would you like to place the Left
value with another Either
which can be another Left
or even Right
? You can use leftFlatMap
.
Left
to Right
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum.leftFlatMap(err -> Either.right(0));
// Either<String, Integer> = Right(0)
Or it can be another Left
like
import j8plus.types.Either;
public class MyError {
public final String message;
public MyError(final String message) {
this.message = message;
}
@Override
public String toString() {
return "MyError(message=" + message + ")";
}
}
final Either<String, Integer> errorMsgOrNum = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum.leftFlatMap(err -> Either.left(new MyError(err)));
// Either<MyError, Integer> = Left(MyError(message=Error message))
Either.swap
If you want to swap Left
and Right
? You can use Either.swap()
.
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.right(1);
// Either<String, Integer> = Right(1)
errorMsgOrNum.swap();
// Either<Integer, String> = Left(1)
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum.swap();
// Either<Integer, String> = Right(Error message)
Get the Value
Either.fold
If you want to get the value inside Either
, you can use Either.fold
. Since you can have only one result type, you have to provide ways to turn a type in Right
and a type in Left
into one result type. So fold
takes two functions. One for the Right
case and the other for the Left
case.
The Right
case,
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.right(999);
// Either<String, Integer> = Right(999)
errorMsgOrNum.fold(err -> 0, n -> n);
// Integer = 999
// Or using the identity function from java.util.function.Function
import java.util.function.Function;
errorMsgOrNum.fold(err -> 0, Function.identity());
// Integer = 999
The Left
case,
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum.fold(err -> 0, n -> n);
// Integer = 0
You can also turn the value into a completely different type.
import j8plus.types.Either;
import java.time.Instant;
final Either<String, Long> errorMsgOrMillis = Either.right(1638316800000L);
// Either<String, Long> = Right(1638316800000L)
errorMsgOrMillis.fold(err -> Instant.now(), millis -> Instant.ofEpochMilli(millis));
// Instant = 2021-12-01T00:00:00Z
// Or using method reference
errorMsgOrMillis.fold(err -> Instant.now(), Instant::ofEpochMilli);
// Instant = 2021-12-01T00:00:00Z
import j8plus.types.Either;
import java.time.Instant;
final Either<String, Long> errorMsgOrMillis = Either.left("Error message");
// Either<String, Long> = Left(Error message)
errorMsgOrMillis.fold(err -> Instant.now(), millis -> Instant.ofEpochMilli(millis));
// Instant = 2021-12-30T09:15:24.033117Z
Either.getOrElse
To get the Right
value, you can use getOrElse(Supplier<AlternativeValue>)
. If the Either
is Left
, it returns the given alternative from the supplier param.
Right
case,
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.right(999);
// Either<String, Integer> = Right(999)
errorMsgOrNum.getOrElse(() -> 0);
// Integer = 999
Left
case,
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum.getOrElse(() -> 0);
// Integer = 0
Either.getLeftOrElse
To get the Left
value, you can use getLeftOrElse(Supplier<AlternativeValue>)
. If the Either
is Right
, it returns the given alternative from the supplier param.
Right
case,
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.right(999);
// Either<String, Integer> = Right(999)
errorMsgOrNum.getLeftOrElse(() -> "Default");
// String = Default
Left
case,
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum.getLeftOrElse(() -> "Default");
// String = Error message
Use Value
Either.forEach
If you want to use the Right
value and don't need to return anything, you can use forEach
.
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.right(999);
// Either<String, Integer> = Right(999)
errorMsgOrNum.forEach(n -> System.out.println("The value is " + n));
// The value is 999
Either.forEachLeft
If you want to use the Left
value and don't need to return anything, you can use forEachLeft
.
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum.forEachLeft(err -> System.out.println("ERROR: " + err));
// ERROR: Error message
Check Value in Either
If you want to chekc if Either
is Right
or Left
, you can use isRight()
and isLeft()
respectively.
Either.isRight
To check if the Either
is Right
, use isRight()
,
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.right(999);
// Either<String, Integer> = Right(999)
errorMsgOrNum.isRight();
// boolean = true
final Either<String, Integer> errorMsgOrNum2 = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum2.isRight();
// boolean = false
Either.isLeft
To check if the Either
is Left
, use isLeft()
,
import j8plus.types.Either;
final Either<String, Integer> errorMsgOrNum = Either.right(999);
// Either<String, Integer> = Right(999)
errorMsgOrNum.isLeft();
// boolean = false
final Either<String, Integer> errorMsgOrNum2 = Either.left("Error message");
// Either<String, Integer> = Left(Error message)
errorMsgOrNum2.isLeft();
// boolean = true
Example
import j8plus.types.Either;
public class DivisionByZeroError {
public final int dividend;
public DivisionByZeroError(final int dividend) {
this.dividend = dividend;
}
public String getMessage() {
return "Arithmetic error: dividing " + dividend + " by zero is not possible.";
}
@Override
public String toString() {
return new StringBuilder("DivisionByZeroError(")
.append("dividend=").append(dividend)
.append(')')
.toString();
}
}
public Either<DivisionByZeroError, Integer> divide(int a, int b) {
if (b == 0) {
return Either.left(new DivisionByZeroError(a));
} else {
return Either.right(a / b);
}
}
final var result1 = divide(10, 2);
// Either<DivisionByZeroError, Integer> = Right(5)
final var result2 = divide(10, 0);
// Either<DivisionByZeroError, Integer> = Left(DivisionByZeroError(dividend=10))