Introduction
In the previous part we explained what is a monad, a monad transformer and demonstrated the importance of the TryT
monad transformer.
In this part we are going to introduce another monad transformer called EitherT
.
What is EitherT
EitherT
is a monad transformer that encapsulates an Either
monad inside a CompletableFuture
.
This combination allows for chaining and composing asynchronous computations that may fail, using functional programming principles. The Either monad represents a value of one of two possible types. Instances of Either
are either an instance of Left or Right.
- Left is typically used to represent an error or failure.
- Right is used to represent a success or valid result.
By wrapping an Either
in a CompletableFuture
, EitherT
enables the handling of asynchronous operations that can fail or succeed in a functional way.
Problems solved by EitherT
- Error Propagation: Propagates errors through the computation chain without the need for weird error-checking code.
- Composability: Allows for the composition of multiple asynchronous operations, each of which may fail, in an easy to work manner.
- Simplified Error Recovery: Provides straightforward mechanisms to recover from errors.
Implementation
import io.vavr.Value; | |
import io.vavr.control.Either; | |
import java.util.Objects; | |
import java.util.concurrent.CompletableFuture; | |
import java.util.function.Function; | |
/** | |
* The {@code EitherT} monad transformer class encapsulates an {@code Either} monad inside a {@code | |
* CompletableFuture}. This allows chaining and composing asynchronous computations that may fail, | |
* using functional programming principles. | |
* | |
* @param <A> the type of the left value | |
* @param <B> the type of the right value | |
*/ | |
public class EitherT<A, B> { | |
private final CompletableFuture<Either<A, B>> future; | |
private EitherT(CompletableFuture<Either<A, B>> future) { | |
this.future = Objects.requireNonNull(future, "future must not be null"); | |
} | |
/** | |
* Constructs an {@code EitherT} instance with a right value. | |
* | |
* @param value the right value | |
* @return an {@code EitherT} instance containing the right value | |
* @param <A> the type of the left value | |
* @param <B> the type of the right value | |
*/ | |
public static <A, B> EitherT<A, B> right(B value) { | |
return new EitherT<>(CompletableFuture.completedFuture(Either.right(value))); | |
} | |
/** | |
* Creates an {@code EitherT} instance representing a failure. | |
* | |
* @param exception the exception to be wrapped | |
* @return an {@code EitherT} instance containing the failure | |
* @param <A> the type of the left value | |
* @param <B> the type of the right value | |
*/ | |
public static <A, B> EitherT<A, B> left(A exception) { | |
return new EitherT<>(CompletableFuture.completedFuture(Either.left(exception))); | |
} | |
/** | |
* Creates an {@code EitherT} instance from a {@code CompletableFuture} of {@code Either}. | |
* | |
* @param future the {@code CompletableFuture} of {@code Either} to be wrapped | |
* @return an {@code EitherT} instance wrapping the given {@code CompletableFuture} | |
* @param <A> the type of the left value | |
* @param <B> the type of the right value | |
*/ | |
public static <A, B> EitherT<A, B> fromFuture(CompletableFuture<Either<A, B>> future) { | |
return new EitherT<>(future); | |
} | |
/** | |
* Maps the right value of the {@code Either} monad. | |
* | |
* @param mapper the function to apply to the right value | |
* @return a new {@code EitherT} instance with the mapped value | |
* @param <C> the type of the new right value | |
*/ | |
public <C> EitherT<A, C> map(Function<B, C> mapper) { | |
Objects.requireNonNull(mapper, "mapper must not be null"); | |
return new EitherT<>(future.thenApply(either -> either.map(mapper))); | |
} | |
/** | |
* Flat maps the right value of the {@code Either} monad. | |
* | |
* @param mapper the function to apply to the right value | |
* @return a new {@code EitherT} instance with the mapped value | |
* @param <C> the type of the new right value | |
*/ | |
public <C> EitherT<A, C> flatMap(Function<B, EitherT<A, C>> mapper) { | |
Objects.requireNonNull(mapper, "mapper must not be null"); | |
return new EitherT<>( | |
future.thenCompose( | |
either -> | |
either.fold( | |
left -> CompletableFuture.completedFuture(Either.left(left)), | |
right -> mapper.apply(right).future))); | |
} | |
/** | |
* Maps both the left and right values of the {@code Either} monad. | |
* | |
* @param leftMapper the function to apply to the left value | |
* @param rightMapper the function to apply to the right value | |
* @return a new {@code EitherT} instance with the mapped values | |
* @param <C> the type of the new left value | |
* @param <D> the type of the new right value | |
*/ | |
public <C, D> EitherT<C, D> biFlatMap( | |
Function<A, EitherT<C, D>> leftMapper, Function<B, EitherT<C, D>> rightMapper) { | |
Objects.requireNonNull(leftMapper, "leftMapper must not be null"); | |
Objects.requireNonNull(rightMapper, "rightMapper must not be null"); | |
return new EitherT<>( | |
future.thenCompose( | |
either -> | |
either.fold( | |
left -> leftMapper.apply(left).getFuture(), | |
right -> rightMapper.apply(right).getFuture()))); | |
} | |
/** | |
* Maps either the left or right values of the {@code Either} monad. | |
* | |
* @param leftMapper the function to apply to the left value | |
* @param rightMapper the function to apply to the right value | |
* @return a new {@code EitherT} instance with the mapped values | |
* @param <C> the type of the new left value | |
* @param <D> the type of the new right value | |
*/ | |
public <C, D> EitherT<C, D> biMap(Function<A, C> leftMapper, Function<B, D> rightMapper) { | |
Objects.requireNonNull(leftMapper, "leftMapper must not be null"); | |
Objects.requireNonNull(rightMapper, "rightMapper must not be null"); | |
return new EitherT<>(future.thenApply(either -> either.bimap(leftMapper, rightMapper))); | |
} | |
/** | |
* Maps the left value of the {@code Either} monad. | |
* | |
* @param leftMapper the function to apply to the left value | |
* @return a new {@code EitherT} instance with the mapped value | |
* @param <C> the type of the new left value | |
*/ | |
public <C> EitherT<C, B> leftMap(Function<A, C> leftMapper) { | |
Objects.requireNonNull(leftMapper, "leftMapper must not be null"); | |
return new EitherT<>(future.thenApply(either -> either.mapLeft(leftMapper))); | |
} | |
/** | |
* Returns the underlying {@code CompletableFuture} of {@code Either}. | |
* | |
* @return the {@code CompletableFuture<Either<A, B>>} representing the asynchronous computation | |
*/ | |
public CompletableFuture<Either<A, B>> getFuture() { | |
return future; | |
} | |
/** | |
* Recovers from a failure by applying a function that returns a new {@code EitherT} instance. | |
* | |
* @param recover the function to apply to the left value | |
* @return a new {@code EitherT} instance with the recovered value | |
*/ | |
public EitherT<A, B> recoverWith(Function<A, EitherT<A, B>> recover) { | |
Objects.requireNonNull(recover, "recover must not be null"); | |
return new EitherT<>( | |
future.thenCompose( | |
either -> | |
either.fold( | |
left -> recover.apply(left).getFuture(), | |
right -> CompletableFuture.completedFuture(Either.right(right))))); | |
} | |
/** | |
* Converts this {@code EitherT} instance to a {@code TryT} instance. | |
* | |
* @return a {@code TryT} instance containing the value of this {@code EitherT} | |
*/ | |
public TryT<B> toTryT() { | |
return TryT.fromFuture(future.thenApply(Value::toTry)); | |
} | |
} |
Examples
- Basic usage
import io.vavr.control.Either;
public class EitherTExample {
public static void main(String[] args) {
EitherT<String, Integer> successfulComputation = EitherT.right(42);
EitherT<String, Integer> failedComputation = EitherT.left("Error occurred");
successfulComputation.getFuture().thenAccept(result ->
System.out.println("Success: " + result)
);
failedComputation.getFuture().thenAccept(result ->
System.out.println("Failure: " + result)
);
}
}
In this example, we create two EitherT
instances: one representing a successful computation and the other a failure. We then use the getFuture() method to access the underlying CompletableFuture
and handle the results accordingly.
- Composing Asychronous computations
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
public class EitherTComposition {
public static void main(String[] args) {
EitherT<String, Integer> computation1 = EitherT.right(10);
EitherT<String, Integer> computation2 = computation1.flatMap(value -> EitherT.right(value * 2));
EitherT<String, Integer> computation3 = computation2.flatMap(value -> EitherT.right(value + 5));
computation3.getFuture().thenAccept(result ->
result.fold(
error -> System.out.println("Error: " + error),
value -> System.out.println("Success: " + value)
)
);
}
}
- Error recovery and handling
import java.util.function.Function;
public class EitherTErrorRecovery {
public static void main(String[] args) {
EitherT<String, Integer> failedComputation = EitherT.left("Initial error");
Function<String, EitherT<String, Integer>> recoverFunction = error -> EitherT.right(100);
EitherT<String, Integer> recoveredComputation = failedComputation.recoverWith(recoverFunction);
recoveredComputation.getFuture().thenAccept(result ->
result.fold(
error -> System.out.println("Error: " + error),
value -> System.out.println("Recovered Success: " + value)
)
);
}
}
Conclusion
The EitherT
class is a powerful tool for managing asynchronous computations and error handling in Java and it shows us that it is not hard to embrace functional programming constructs and philosophy in Java.
Top comments (0)