Introduction
During software engineering there is often a need to handle tasks that run in the background and might fail. Using CompletableFuture helps with running tasks asynchronously, and Try from the Vavr library helps manage errors in a functional way. But combining these can make the code complex. This article introduces TryT
, a special tool that wraps Try inside CompletableFuture
. This makes it easier to handle both asynchronous tasks and errors together.
What is a Monad?
A monad is a pattern used in functional programming that helps manage computations and data transformations. Think of it as a wrapper around a value or a task that provides a structured way to handle operations on that value.
For example, in Java, Optional
is a monad. It wraps a value that might or might not be there and provides methods like map to transform the value and flatMap to chain operations.
What is a Monad Transformer?
A monad transformer combines two monads, allowing you to work with both at the same time without getting confused. If you have a CompletableFuture for asynchronous tasks and a Try for handling errors, a monad transformer like TryT wraps them together so you can manage both effects more easily.
What is TryT?
TryT is a tool that combines Try and CompletableFuture. It helps you handle tasks that run in the background and might fail. TryT makes it simpler to chain these tasks and manage errors in a clean way. The name follows the naming conventions used by functional libraries in regards with monad transformers by adding a T suffix.
Why Use TryT?
Directly working with CompletableFuture<Try<T>>
can make your code complex and hard to read. TryT simplifies this by:
- Combining Error and Async Handling: It handles both errors and asynchronous tasks together.
- Cleaner Code: Makes your code easier to read and maintain.
- Easier to Chain Tasks: Helps you chain tasks without writing a lot of extra code.
Implementation
import io.vavr.control.Try; | |
import java.util.concurrent.CompletableFuture; | |
import java.util.function.Function; | |
/** | |
* The {@code TryT} monad transformer class encapsulates a {@code Try} monad inside a {@code | |
* CompletableFuture}. This allows chaining and composing asynchronous computations that may fail, | |
* using functional programming principles. | |
* | |
* <p>This class provides methods to transform and compose the inner {@code Try} monad values | |
* through asynchronous operations. It supports operations such as {@link #map(Function)} and {@link | |
* #flatMap(Function)}, which enable you to perform transformations and handle the potential | |
* failures of asynchronous computations in a clean and expressive manner. | |
* | |
* @param <T> the type of the value contained within the {@code TryT} monad | |
*/ | |
public class TryT<T> { | |
private final CompletableFuture<Try<T>> future; | |
/** | |
* Constructs a {@code TryT} instance with a {@code CompletableFuture} of {@code Try}. | |
* | |
* @param future the {@code CompletableFuture<Try<T>>} representing the asynchronous computation | |
*/ | |
private TryT(CompletableFuture<Try<T>> future) { | |
this.future = future; | |
} | |
/** | |
* Creates a {@code TryT} instance representing a successful value. | |
* | |
* @param value the value to be wrapped | |
* @param <T> the type of the value | |
* @return a {@code TryT} instance containing the successful value | |
*/ | |
public static <T> TryT<T> of(T value) { | |
return new TryT<>(CompletableFuture.completedFuture(Try.success(value))); | |
} | |
/** | |
* Creates a {@code TryT} instance representing a failure. | |
* | |
* @param exception the exception to be wrapped | |
* @param <T> the type of the value | |
* @return a {@code TryT} instance containing the failure | |
*/ | |
public static <T> TryT<T> ofFailure(Throwable exception) { | |
return new TryT<>(CompletableFuture.completedFuture(Try.failure(exception))); | |
} | |
/** | |
* Creates a {@code TryT} instance from a {@code CompletableFuture} of {@code Try}. | |
* | |
* @param future the {@code CompletableFuture} of {@code Try} to be wrapped | |
* @param <T> the type of the value | |
* @return a {@code TryT} instance wrapping the given {@code CompletableFuture} | |
*/ | |
public static <T> TryT<T> fromFuture(CompletableFuture<Try<T>> future) { | |
return new TryT<>(future); | |
} | |
/** | |
* Returns the underlying {@code CompletableFuture} of {@code Try}. | |
* | |
* @return the {@code CompletableFuture<Try<T>>} representing the asynchronous computation | |
*/ | |
public CompletableFuture<Try<T>> toCompletableFuture() { | |
return future; | |
} | |
/** | |
* Transforms the value contained within this {@code TryT} instance using the given mapping | |
* function. | |
* | |
* <p>If this {@code TryT} contains a failure, the failure is propagated without applying the | |
* mapping function. | |
* | |
* @param mapper the function to apply to the contained value | |
* @param <U> the type of the new value | |
* @return a new {@code TryT} instance containing the transformed value | |
*/ | |
public <U> TryT<U> map(Function<? super T, ? extends U> mapper) { | |
return new TryT<>(future.thenApply(t -> t.map(mapper))); | |
} | |
/** | |
* Flat maps the value contained within this {@code TryT} instance using the given mapping | |
* function that returns a new {@code TryT}. | |
* | |
* <p>If this {@code TryT} contains a failure, the failure is propagated without applying the | |
* mapping function. | |
* | |
* @param mapper the function to apply to the contained value, returning a new {@code TryT} | |
* @param <U> the type of the new value | |
* @return a new {@code TryT} instance containing the transformed value | |
*/ | |
public <U> TryT<U> flatMap(Function<? super T, TryT<U>> mapper) { | |
return new TryT<>( | |
future.thenCompose( | |
t -> | |
t.isSuccess() | |
? mapper.apply(t.get()).toCompletableFuture() | |
: CompletableFuture.completedFuture(Try.failure(t.getCause())))); | |
} | |
/** | |
* Recovers from a failure by applying the given function to the exception. | |
* | |
* @param recoverFunction the function to apply to the exception | |
* @return a new {@code TryT} instance with the recovered value | |
*/ | |
public TryT<T> recover(Function<Throwable, T> recoverFunction) { | |
return new TryT<>(future.thenApply(t -> t.recover(recoverFunction))); | |
} | |
} |
Examples
Transforming values
- Using
CompletableFuture<Try<T>>
directly:
CompletableFuture<Try<String>> futureTry = someAsyncOperation();
CompletableFuture<Try<Integer>> result = futureTry.thenApply(tryValue -> {
return tryValue.map(String::length);
});
Whereas with TryT
:
TryT<String> tryT = TryT.fromFuture(someAsyncOperation());
TryT<Integer> result = tryT.map(String::length);
- Chaining Asynchronous Operations
CompletableFuture<Try<String>> futureTry = someAsyncOperation();
CompletableFuture<Try<Integer>> result = futureTry.thenCompose(tryValue -> {
if (tryValue.isSuccess()) {
return someOtherAsyncOperation(tryValue.get())
.thenApply(Try::success)
.exceptionally(Try::failure);
} else {
return CompletableFuture.completedFuture(Try.failure(tryValue.getCause()));
}
});
Whereas with TryT
TryT<String> tryT = TryT.fromFuture(someAsyncOperation());
TryT<Integer> result = tryT.flatMap(value -> TryT.fromFuture(someOtherAsyncOperation(value)));
- Error recovery
CompletableFuture<Try<String>> futureTry = someAsyncOperation();
CompletableFuture<Try<String>> recovered = futureTry.thenApply(tryValue -> {
return tryValue.recover(ex -> "Fallback value");
});
Whereas with TryT
TryT<String> tryT = TryT.fromFuture(someAsyncOperation());
TryT<String> recovered = tryT.recover(ex -> "Fallback value");
Conclusion
The TryT monad transformer helps you manage asynchronous tasks and errors together in a simpler way. By combining Try with CompletableFuture, TryT provides a clean and functional approach to handle both errors and asychronous tasks. This makes your code easier to read and maintain.
Top comments (0)