DEV Community

Vasilis Soumakis
Vasilis Soumakis

Posted on

3 1 1 1 1

Monad Transformer in Java for handling Asynchronous Operations and errors

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:

  1. Combining Error and Async Handling: It handles both errors and asynchronous tasks together.
  2. Cleaner Code: Makes your code easier to read and maintain.
  3. 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)));
}
}
view raw TryT.java hosted with ❤ by GitHub

Examples

Transforming values

  1. Using CompletableFuture<Try<T>> directly:
CompletableFuture<Try<String>> futureTry = someAsyncOperation();

CompletableFuture<Try<Integer>> result = futureTry.thenApply(tryValue -> {
    return tryValue.map(String::length);
});
Enter fullscreen mode Exit fullscreen mode

Whereas with TryT:

TryT<String> tryT = TryT.fromFuture(someAsyncOperation());

TryT<Integer> result = tryT.map(String::length);
Enter fullscreen mode Exit fullscreen mode
  1. 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()));
    }
});
Enter fullscreen mode Exit fullscreen mode

Whereas with TryT

TryT<String> tryT = TryT.fromFuture(someAsyncOperation());

TryT<Integer> result = tryT.flatMap(value -> TryT.fromFuture(someOtherAsyncOperation(value)));
Enter fullscreen mode Exit fullscreen mode
  1. Error recovery
CompletableFuture<Try<String>> futureTry = someAsyncOperation();

CompletableFuture<Try<String>> recovered = futureTry.thenApply(tryValue -> {
    return tryValue.recover(ex -> "Fallback value");
});
Enter fullscreen mode Exit fullscreen mode

Whereas with TryT

TryT<String> tryT = TryT.fromFuture(someAsyncOperation());

TryT<String> recovered = tryT.recover(ex -> "Fallback value");
Enter fullscreen mode Exit fullscreen mode

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.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay