Higher-Kinded Types - Basic Usage Examples
This document provides a brief summary of the example classes found in the
org.higherkindedj.example.basic
package in the HKJ-Examples.
These examples showcase how to use various monads and monad transformers to handle common programming tasks like managing optional values, asynchronous operations, and state in a functional way.
Monads
EitherExample.java
This example demonstrates the Either monad. Either
is used to represent a value that can be one of two types, typically a success value (Right
) or an error value (Left
).
- Key Concept: A
Either
provides a way to handle computations that can fail with a specific error type. - Demonstrates:
- Creating
Either
instances for success (Right
) and failure (Left
) cases. - Using
flatMap
to chain operations that return anEither
, short-circuiting on failure. - Using
fold
to handle both theLeft
andRight
cases.
- Creating
// Chain operations that can fail
Either<String, Integer> result = input.flatMap(parse).flatMap(checkPositive);
// Fold to handle both outcomes
String message = result.fold(
leftValue -> "Operation failed with: " + leftValue,
rightValue -> "Operation succeeded with: " + rightValue
);
ForComprehensionExample.java
This example demonstrates how to use the For
comprehension, a feature that provides a more readable, sequential syntax for composing monadic operations (equivalent to flatMap
chains).
- Key Concept: A
For
comprehension offers syntactic sugar forflatMap
andmap
calls, making complex monadic workflows easier to write and understand. - Demonstrates:
- Using
For.from()
to start and chain monadic operations. - Applying comprehensions to different monads like
List
,Maybe
, and theStateT
monad transformer. - Filtering intermediate results with
.when()
. - Introducing intermediate values with
.let()
. - Producing a final result with
.yield()
.
- Using
// A for-comprehension with List
final Kind<ListKind.Witness, String> result =
For.from(listMonad, list1)
.from(_ -> list2)
.when(t -> (t._1() + t._2()) % 2 != 0) // Filter
.let(t -> "Sum: " + (t._1() + t._2())) // Introduce new value
.yield((a, b, c) -> a + " + " + b + " = " + c); // Final result
CompletableFutureExample.java
This example covers the CompletableFuture monad. It shows how to use CompletableFuture
within the Higher-Kinded-J framework to manage asynchronous computations and handle potential errors.
- Key Concept: The
CompletableFuture
monad is used to compose asynchronous operations in a non-blocking way. - Demonstrates:
- Creating
Kind
-wrappedCompletableFuture
instances for success and failure. - Using
map
(which corresponds tothenApply
). - Using
flatMap
(which corresponds tothenCompose
) to chain dependent asynchronous steps. - Using
handleErrorWith
to recover from exceptions that occur within the future.
- Creating
// Using handleErrorWith to recover from a failed future
Function<Throwable, Kind<CompletableFutureKind.Witness, String>> recoveryHandler =
error -> {
System.out.println("Handling error: " + error.getMessage());
return futureMonad.of("Recovered from Error");
};
Kind<CompletableFutureKind.Witness, String> recoveredFuture =
futureMonad.handleErrorWith(failedFutureKind, recoveryHandler);
IdExample.java
This example introduces the Identity (Id) monad. The Id
monad is the simplest monad; it wraps a value without adding any computational context. It is primarily used to make generic code that works with any monad also work with simple, synchronous values.
- Key Concept: The
Id
monad represents a direct, synchronous computation. It wraps a value, and itsflatMap
operation simply applies the function to the value. - Demonstrates:
- Wrapping a plain value into an
Id
. - Using
map
andflatMap
on anId
value. - Its use as the underlying monad in a monad transformer stack, effectively turning
StateT<S, Id.Witness, A>
intoState<S, A>
.
- Wrapping a plain value into an
// flatMap on Id simply applies the function to the wrapped value.
Id<String> idFromOf = Id.of(42);
Id<String> directFlatMap = idFromOf.flatMap(i -> Id.of("Direct FlatMap: " + i));
// directFlatMap.value() is "Direct FlatMap: 42"
IOExample.java
This example introduces the IO monad, which is used to encapsulate side effects like reading from the console, writing to a file, or making a network request.
- Key Concept: The
IO
monad describes a computation that can perform side effects. These effects are only executed when theIO
action is explicitly run. - Demonstrates:
- Creating
IO
actions that describe side effects usingdelay
. - Composing
IO
actions usingmap
andflatMap
to create more complex programs. - Executing
IO
actions to produce a result usingunsafeRunSync
.
- Creating
// Create an IO action to read a line from the console
Kind<IOKind.Witness, String> readLine = IO_OP.delay(() -> {
System.out.print("Enter your name: ");
try (Scanner scanner = new Scanner(System.in)) {
return scanner.nextLine();
}
});
// Execute the action to get the result
String name = IO_OP.unsafeRunSync(readLine);
LazyExample.java
This example covers the Lazy monad. It's used to defer a computation until its result is explicitly requested. The result is then memoized (cached) so the computation is only executed once.
- Key Concept: A
Lazy
computation is not executed when it is created, but only whenforce()
is called. The result (or exception) is then stored for subsequent calls. - Demonstrates:
- Creating a deferred computation with
LAZY.defer()
. - Forcing evaluation with
LAZY.force()
. - How results are memoized, preventing re-computation.
- Using
map
andflatMap
to build chains of lazy operations.
- Creating a deferred computation with
// Defer a computation
java.util.concurrent.atomic.AtomicInteger counter = new java.util.concurrent.atomic.AtomicInteger(0);
Kind<LazyKind.Witness, String> deferredLazy = LAZY.defer(() -> {
counter.incrementAndGet();
return "Computed Value";
});
// The computation only runs when force() is called
System.out.println(LAZY.force(deferredLazy)); // counter becomes 1
System.out.println(LAZY.force(deferredLazy)); // result is from cache, counter remains 1
ListMonadExample.java
This example demonstrates the List monad. It shows how to perform monadic operations on a standard Java List
, treating it as a context that can hold zero or more results.
- Key Concept: The
List
monad represents non-deterministic computation, where an operation can produce multiple results. - Demonstrates:
- Wrapping a
List
into aKind<ListKind.Witness, A>
. - Using
map
to transform every element in the list. - Using
flatMap
to apply a function that returns a list to each element, and then flattening the result.
- Wrapping a
// A function that returns multiple results for even numbers
Function<Integer, Kind<ListKind.Witness, Integer>> duplicateIfEven =
n -> {
if (n % 2 == 0) {
return LIST.widen(Arrays.asList(n, n * 10));
} else {
return LIST.widen(List.of()); // Empty list for odd numbers
}
};
// flatMap applies the function and flattens the resulting lists
Kind<ListKind.Witness, Integer> flatMappedKind = listMonad.flatMap(duplicateIfEven, numbersKind);
MaybeExample.java
This example covers the Maybe monad. Maybe
is a type that represents an optional value, similar to Java's Optional
, but designed to be used as a monad within the Higher-Kinded-J ecosystem. It has two cases: Just<A>
(a value is present) and Nothing
(a value is absent).
- Key Concept: The
Maybe
monad provides a way to represent computations that may or may not return a value, explicitly handling the absence of a value. - Demonstrates:
- Creating
Just
andNothing
instances. - Using
map
to transform aJust
value. - Using
flatMap
to chain operations that return aMaybe
. - Handling the
Nothing
case usinghandleErrorWith
.
- Creating
// flatMap to parse a string, which can result in Nothing
Function<String, Kind<MaybeKind.Witness, Integer>> parseString =
s -> {
try {
return MAYBE.just(Integer.parseInt(s));
} catch (NumberFormatException e) {
return MAYBE.nothing();
}
};
OptionalExample.java
This example introduces the Optional monad. It demonstrates how to wrap Java's Optional
in a Kind
to work with it in a monadic way, allowing for chaining of operations and explicit error handling.
- Key Concept: The
Optional
monad provides a way to represent computations that may or may not return a value. - Demonstrates:
- Wrapping
Optional
instances into aKind<OptionalKind.Witness, A>
. - Using
map
to transform the value inside a presentOptional
. - Using
flatMap
to chain operations that returnOptional
. - Using
handleErrorWith
to provide a default value when theOptional
is empty.
- Wrapping
// Using flatMap to parse a string to an integer, which may fail
Function<String, Kind<OptionalKind.Witness, Integer>> parseToIntKind =
s -> {
try {
return OPTIONAL.widen(Optional.of(Integer.parseInt(s)));
} catch (NumberFormatException e) {
return OPTIONAL.widen(Optional.empty());
}
};
Kind<OptionalKind.Witness, Integer> parsedPresent =
optionalMonad.flatMap(parseToIntKind, presentInput);
ReaderExample.java
This example introduces the Reader monad. The Reader
monad is a pattern used for dependency injection. It represents a computation that depends on some configuration or environment of type R
.
- Key Concept: A
Reader<R, A>
represents a functionR -> A
. It allows you to "read" from a configurationR
to produce a valueA
, without explicitly passing the configuration object everywhere. - Demonstrates:
- Creating
Reader
computations that access parts of a configuration object. - Using
flatMap
to chain computations where one step depends on the result of a previous step and the shared configuration. - Running the final
Reader
computation by providing a concrete configuration object.
- Creating
// A Reader that depends on the AppConfig environment
Kind<ReaderKind.Witness<AppConfig>, String> connectionStringReader =
readerMonad.flatMap(
dbUrl -> READER.reader(config -> dbUrl + "?apiKey=" + config.apiKey()),
getDbUrl // Another Reader that gets the DB URL
);
// The computation is only run when a config is provided
String connectionString = READER.runReader(connectionStringReader, productionConfig);
StateExample, BankAccountWorkflow.java
These examples demonstrate the State monad. The State
monad is used to manage state in a purely functional way, abstracting away the boilerplate of passing state from one function to the next.
- Key Concept: A
State<S, A>
represents a functionS -> (S, A)
, which takes an initial state and returns a new state and a computed value. The monad chains these functions together. - Demonstrates:
- Creating stateful actions like
push
,pop
,deposit
, andwithdraw
. - Using
State.modify
to update the state andState.inspect
to read from it. - Composing these actions into a larger workflow using a
For
comprehension. - Running the final computation with an initial state to get the final state and result.
- Creating stateful actions like
// A stateful action to withdraw money, returning a boolean success flag
public static Function<BigDecimal, Kind<StateKind.Witness<AccountState>, Boolean>> withdraw(String description) {
return amount -> STATE.widen(
State.of(currentState -> {
if (currentState.balance().compareTo(amount) >= 0) {
// ... update state and return success
return new StateTuple<>(true, updatedState);
} else {
// ... update state with rejection and return failure
return new StateTuple<>(false, updatedState);
}
})
);
}
TryExample.java
This example introduces the Try monad. It's designed to encapsulate computations that can throw exceptions, making error handling more explicit and functional.
- Key Concept: A
Try
represents a computation that results in either aSuccess
containing a value or aFailure
containing an exception. - Demonstrates:
- Creating
Try
instances for successful and failed computations. - Using
map
andflatMap
to chain operations, where exceptions are caught and wrapped in aFailure
. - Using
recover
andrecoverWith
to handle failures and provide alternative values or computations.
- Creating
// A function that returns a Try, succeeding or failing based on the input
Function<Integer, Try<Double>> safeDivide =
value ->
(value == 0)
? Try.failure(new ArithmeticException("Div by zero"))
: Try.success(10.0 / value);
// flatMap chains the operation, propagating failure
Try<Double> result = input.flatMap(safeDivide);
ValidatedMonadExample.java
This example showcases the Validated applicative functor. While it has a Monad
instance, it's often used as an Applicative
to accumulate errors. This example, however, focuses on its monadic (fail-fast) behavior.
- Key Concept:
Validated
is used for validation scenarios where you want to either get a valid result or a collection of validation errors. - Demonstrates:
- Creating
Valid
andInvalid
instances. - Using
flatMap
to chain validation steps, where the firstInvalid
result short-circuits the computation. - Using
handleErrorWith
to recover from a validation failure.
- Creating
// A validation function that returns a Kind-wrapped Validated
Function<String, Kind<ValidatedKind.Witness<List<String>>, Integer>> parseToIntKind =
s -> {
try {
return validatedMonad.of(Integer.parseInt(s)); // Lifts to Valid
} catch (NumberFormatException e) {
return validatedMonad.raiseError(Collections.singletonList("'" + s + "' is not a number."));
}
};
WriterExample.java
This example introduces the Writer monad. The Writer
monad is used for computations that need to produce a log or accumulate a secondary value alongside their primary result.
- Key Concept: A
Writer<W, A>
represents a computation that returns a primary resultA
and an accumulated valueW
(like a log), whereW
must have aMonoid
instance to define how values are combined. - Demonstrates:
- Using
tell
to append to the log. - Using
flatMap
to sequence computations, where both the results and logs are combined automatically. - Running the final
Writer
to extract both the final value and the fully accumulated log.
- Using
// An action that performs a calculation and logs what it did
Function<Integer, Kind<WriterKind.Witness<String>, Integer>> addAndLog =
x -> {
int result = x + 10;
String logMsg = "Added 10 to " + x + " -> " + result + "; ";
return WRITER.widen(new Writer<>(logMsg, result));
};
// The monad combines the logs from each step automatically
Kind<WriterKind.Witness<String>, String> finalComputation = writerMonad.flatMap(
intermediateValue -> multiplyAndLogToString.apply(intermediateValue),
addAndLog.apply(5)
);
GenericExample.java
This example showcases how to write generic functions that can operate on any Functor
(or Monad
) by accepting the type class instance as a parameter. This is a core concept of higher-kinded polymorphism.
- Key Concept: By abstracting over the computational context (
F
), you can write code that works forList
,Optional
,IO
, or any other type that has aFunctor
instance. - Demonstrates:
- Writing a generic
mapWithFunctor
function that takes aFunctor<F>
instance and aKind<F, A>
. - Calling this generic function with different monad instances (
ListMonad
,OptionalMonad
) and their correspondingKind
-wrapped types.
- Writing a generic
// A generic function that works for any Functor F
public static <F, A, B> Kind<F, B> mapWithFunctor(
Functor<F> functorInstance, // The type class instance
Function<A, B> fn,
Kind<F, A> kindBox) { // The value in its context
return functorInstance.map(fn, kindBox);
}
// Calling it with a List
Kind<ListKind.Witness, Integer> doubledList = mapWithFunctor(listMonad, doubleFn, listKind);
// Calling it with an Optional
Kind<OptionalKind.Witness, Integer> doubledOpt = mapWithFunctor(optionalMonad, doubleFn, optKind);
Monad Transformers
These examples show how to use monad transformers (EitherT
, MaybeT
, OptionalT
, ReaderT
, StateT
) to combine the capabilities of different monads.
EitherTExample.java
- Key Concept:
EitherT
stacks theEither
monad on top of another monadF
, creating a new monadEitherT<F, L, R>
that handles both the effects ofF
and the failure logic ofEither
. - Scenario: Composing synchronous validation (
Either
) with an asynchronous operation (CompletableFuture
) in a single, clean workflow.
MaybeTExample.java
- Key Concept:
MaybeT
stacks theMaybe
monad on top of another monadF
. This is useful for asynchronous operations that may not return a value. - Scenario: Fetching a user and their preferences from a database asynchronously, where each step might not find a result.
OptionalTExample.java
- Key Concept:
OptionalT
stacksOptional
on top of another monadF
, creatingOptionalT<F, A>
to handle asynchronous operations that may return an empty result. - Scenario: Fetching a user and their preferences from a database asynchronously, where each step might not find a result.
ReaderTExample.java, ReaderTUnitExample.java, ReaderTAsyncUnitExample.java
- Key Concept:
ReaderT
combines theReader
monad (for dependency injection) with an outer monadF
. This allows for computations that both read from a shared environment and have effects of typeF
. - Scenario: An asynchronous workflow that depends on a configuration object (
AppConfig
) to fetch and process data.
StateTExample.java, StateTStackExample
- Key Concept:
StateT
combines theState
monad with an outer monadF
. This is for stateful computations that also involve effects fromF
. - Scenario: A stateful stack that can fail (using
Optional
as the outer monad), where popping from an empty stack results inOptional.empty()
.