CompletableFuture: Asynchronous Computations with CompletableFuture
Java's java.util.concurrent.CompletableFuture<T>
is a powerful tool for asynchronous programming. The higher-kinded-j
library provides a way to treat CompletableFuture
as a monadic context using the HKT simulation. This allows developers to compose asynchronous operations and handle their potential failures (Throwable
) in a more functional and generic style, leveraging type classes like Functor
, Applicative
, Monad
, and crucially, MonadError
.
Higher-Kinded Bridge for CompletableFuture
TypeClasses
The simulation for CompletableFuture
involves these components:
CompletableFuture<A>
: The standard Java class representing an asynchronous computation that will eventually result in a value of typeA
or fail with an exception (aThrowable
).CompletableFutureKind<A>
: The HKT marker interface (Kind<CompletableFutureKind.Witness, A>
) forCompletableFuture
. This allowsCompletableFuture
to be used generically with type classes. The witness type isCompletableFutureKind.Witness
.CompletableFutureKindHelper
: The utility class for bridging betweenCompletableFuture<A>
andCompletableFutureKind<A>
. Key methods:widen(CompletableFuture<A>)
: Wraps a standardCompletableFuture
into itsKind
representation.narrow(Kind<CompletableFutureKind.Witness, A>)
: Unwraps theKind
back to the concreteCompletableFuture
. ThrowsKindUnwrapException
if the input Kind is invalid.join(Kind<CompletableFutureKind.Witness, A>)
: A convenience method to unwrap theKind
and then block (join()
) on the underlyingCompletableFuture
to get its result. It re-throws runtime exceptions and errors directly but wraps checked exceptions inCompletionException
. Use primarily for testing or at the very end of an application where blocking is acceptable.
CompletableFutureFunctor
: ImplementsFunctor<CompletableFutureKind.Witness>
. Providesmap
, which corresponds toCompletableFuture.thenApply()
.CompletableFutureApplicative
: ExtendsFunctor
, implementsApplicative<CompletableFutureKind.Witness>
.of(A value)
: Creates an already successfully completedCompletableFutureKind
usingCompletableFuture.completedFuture(value)
.ap(Kind<F, Function<A,B>>, Kind<F, A>)
: Corresponds toCompletableFuture.thenCombine()
, applying a function from one future to the value of another when both complete.
CompletableFutureMonad
: ExtendsApplicative
, implementsMonad<CompletableFutureKind.Witness>
.flatMap(Function<A, Kind<F, B>>, Kind<F, A>)
: Corresponds toCompletableFuture.thenCompose()
, sequencing asynchronous operations where one depends on the result of the previous one.
CompletableFutureMonad
: ExtendsMonad
, implementsMonadError<CompletableFutureKind.Witness, Throwable>
. This is often the most useful instance to work with.raiseError(Throwable error)
: Creates an already exceptionally completedCompletableFutureKind
usingCompletableFuture.failedFuture(error)
.handleErrorWith(Kind<F, A>, Function<Throwable, Kind<F, A>>)
: Corresponds toCompletableFuture.exceptionallyCompose()
, allowing asynchronous recovery from failures.
Purpose and Usage
- Functional Composition of Async Ops: Use
map
,ap
, andflatMap
(via the type class instances) to build complex asynchronous workflows in a declarative style, similar to how you'd compose synchronous operations withOptional
orList
. - Unified Error Handling: Treat asynchronous failures (
Throwable
) consistently usingMonadError
operations (raiseError
,handleErrorWith
). This allows integrating error handling directly into the composition chain. - HKT Integration: Enables writing generic code that can operate on
CompletableFuture
alongside other simulated monadic types (likeOptional
,Either
,IO
) by programming against theKind<F, A>
interface and type classes. This is powerfully demonstrated when usingCompletableFutureKind
as the outer monadF
in theEitherT
transformer (see Order Example Walkthrough).
Examples
public void createExample() {
// Get the MonadError instance
CompletableFutureMonad futureMonad = CompletableFutureMonad.INSTANCE;
// --- Using of() ---
// Creates a Kind wrapping an already completed future
Kind<CompletableFutureKind.Witness, String> successKind = futureMonad.of("Success!");
// --- Using raiseError() ---
// Creates a Kind wrapping an already failed future
RuntimeException error = new RuntimeException("Something went wrong");
Kind<CompletableFutureKind.Witness, String> failureKind = futureMonad.raiseError(error);
// --- Wrapping existing CompletableFutures ---
CompletableFuture<Integer> existingFuture = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) { /* ignore */ }
return 123;
});
Kind<CompletableFutureKind.Witness, Integer> wrappedExisting = FUTURE.widen(existingFuture);
CompletableFuture<Integer> failedExisting = new CompletableFuture<>();
failedExisting.completeExceptionally(new IllegalArgumentException("Bad input"));
Kind<CompletableFutureKind.Witness, Integer> wrappedFailed = FUTURE.widen(failedExisting);
// You typically don't interact with 'unwrap' unless needed at boundaries or for helper methods like 'join'.
CompletableFuture<String> unwrappedSuccess = FUTURE.narrow(successKind);
CompletableFuture<String> unwrappedFailure = FUTURE.narrow(failureKind);
}
These examples show how to use the type class instance (futureMonad
) to apply operations.
public void monadExample() {
// Get the MonadError instance
CompletableFutureMonad futureMonad = CompletableFutureMonad.INSTANCE;
// --- map (thenApply) ---
Kind<CompletableFutureKind.Witness, Integer> initialValueKind = futureMonad.of(10);
Kind<CompletableFutureKind.Witness, String> mappedKind = futureMonad.map(
value -> "Result: " + value,
initialValueKind
);
// Join for testing/demonstration
System.out.println("Map Result: " + FUTURE.join(mappedKind)); // Output: Result: 10
// --- flatMap (thenCompose) ---
// Function A -> Kind<F, B>
Function<String, Kind<CompletableFutureKind.Witness, String>> asyncStep2 =
input -> FUTURE.widen(
CompletableFuture.supplyAsync(() -> input + " -> Step2 Done")
);
Kind<CompletableFutureKind.Witness, String> flatMappedKind = futureMonad.flatMap(
asyncStep2,
mappedKind // Result from previous map step ("Result: 10")
);
System.out.println("FlatMap Result: " + FUTURE.join(flatMappedKind)); // Output: Result: 10 -> Step2 Done
// --- ap (thenCombine) ---
Kind<CompletableFutureKind.Witness, Function<Integer, String>> funcKind = futureMonad.of(i -> "FuncResult:" + i);
Kind<CompletableFutureKind.Witness, Integer> valKind = futureMonad.of(25);
Kind<CompletableFutureKind.Witness, String> apResult = futureMonad.ap(funcKind, valKind);
System.out.println("Ap Result: " + FUTURE.join(apResult)); // Output: FuncResult:25
// --- mapN ---
Kind<CompletableFutureKind.Witness, Integer> f1 = futureMonad.of(5);
Kind<CompletableFutureKind.Witness, String> f2 = futureMonad.of("abc");
BiFunction<Integer, String, String> combine = (i, s) -> s + i;
Kind<CompletableFutureKind.Witness, String> map2Result = futureMonad.map2(f1, f2, combine);
System.out.println("Map2 Result: " + FUTURE.join(map2Result)); // Output: abc5
}
This is where CompletableFutureMonad
shines, providing functional error recovery.
public void errorHandlingExample(){
// Get the MonadError instance
CompletableFutureMonad futureMonad = CompletableFutureMonad.INSTANCE;
RuntimeException runtimeEx = new IllegalStateException("Processing Failed");
IOException checkedEx = new IOException("File Not Found");
Kind<CompletableFutureKind.Witness, String> failedRuntimeKind = futureMonad.raiseError(runtimeEx);
Kind<CompletableFutureKind.Witness, String> failedCheckedKind = futureMonad.raiseError(checkedEx);
Kind<CompletableFutureKind.Witness, String> successKind = futureMonad.of("Original Success");
// --- Handler Function ---
// Function<Throwable, Kind<CompletableFutureKind.Witness, String>>
Function<Throwable, Kind<CompletableFutureKind.Witness, String>> recoveryHandler =
error -> {
System.out.println("Handling error: " + error.getMessage());
if (error instanceof IOException) {
// Recover from specific checked exceptions
return futureMonad.of("Recovered from IO Error");
} else if (error instanceof IllegalStateException) {
// Recover from specific runtime exceptions
return FUTURE.widen(CompletableFuture.supplyAsync(()->{
System.out.println("Async recovery..."); // Recovery can be async too!
return "Recovered from State Error (async)";
}));
} else if (error instanceof ArithmeticException) {
// Recover from ArithmeticException
return futureMonad.of("Recovered from Arithmetic Error: " + error.getMessage());
}
else {
// Re-raise unhandled errors
System.out.println("Unhandled error type: " + error.getClass().getSimpleName());
return futureMonad.raiseError(new RuntimeException("Recovery failed", error));
}
};
// --- Applying Handler ---
// Handle RuntimeException
Kind<CompletableFutureKind.Witness, String> recoveredRuntime = futureMonad.handleErrorWith(
failedRuntimeKind,
recoveryHandler
);
System.out.println("Recovered (Runtime): " + FUTURE.join(recoveredRuntime));
// Output:
// Handling error: Processing Failed
// Async recovery...
// Recovered (Runtime): Recovered from State Error (async)
// Handle CheckedException
Kind<CompletableFutureKind.Witness, String> recoveredChecked = futureMonad.handleErrorWith(
failedCheckedKind,
recoveryHandler
);
System.out.println("Recovered (Checked): " + FUTURE.join(recoveredChecked));
// Output:
// Handling error: File Not Found
// Recovered (Checked): Recovered from IO Error
// Handler is ignored for success
Kind<CompletableFutureKind.Witness, String> handledSuccess = futureMonad.handleErrorWith(
successKind,
recoveryHandler // This handler is never called
);
System.out.println("Handled (Success): " + FUTURE.join(handledSuccess));
// Output: Handled (Success): Original Success
// Example of re-raising an unhandled error
ArithmeticException unhandledEx = new ArithmeticException("Bad Maths");
Kind<CompletableFutureKind.Witness, String> failedUnhandledKind = futureMonad.raiseError(unhandledEx);
Kind<CompletableFutureKind.Witness, String> failedRecovery = futureMonad.handleErrorWith(
failedUnhandledKind,
recoveryHandler
);
try {
FUTURE.join(failedRecovery);
} catch (CompletionException e) { // join wraps the "Recovery failed" exception
System.err.println("Caught re-raised error: " + e.getCause());
System.err.println(" Original cause: " + e.getCause().getCause());
}
// Output:
// Handling error: Bad Maths
}
handleErrorWith
allows you to inspect theThrowable
and return a newCompletableFutureKind
, potentially recovering the flow.- The handler receives the cause of the failure (unwrapped from
CompletionException
if necessary).