Higher-Kinded Types - Basic Usage Examples
This document provides a brief summary of the example classes found in the
org.higherkindedj.example.basicpackage 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
Eitherprovides a way to handle computations that can fail with a specific error type. - Demonstrates:
- Creating
Eitherinstances for success (Right) and failure (Left) cases. - Using
flatMapto chain operations that return anEither, short-circuiting on failure. - Using
foldto handle both theLeftandRightcases.
- 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
Forcomprehension offers syntactic sugar forflatMapandmapcalls, 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 theStateTmonad 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
CompletableFuturemonad is used to compose asynchronous operations in a non-blocking way. - Demonstrates:
- Creating
Kind-wrappedCompletableFutureinstances for success and failure. - Using
map(which corresponds tothenApply). - Using
flatMap(which corresponds tothenCompose) to chain dependent asynchronous steps. - Using
handleErrorWithto 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
Idmonad represents a direct, synchronous computation. It wraps a value, and itsflatMapoperation simply applies the function to the value. - Demonstrates:
- Wrapping a plain value into an
Id. - Using
mapandflatMapon anIdvalue. - Its use as the underlying monad in a monad transformer stack, effectively turning
StateT<S, IdKind.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
IOmonad describes a computation that can perform side effects. These effects are only executed when theIOaction is explicitly run. - Demonstrates:
- Creating
IOactions that describe side effects usingdelay. - Composing
IOactions usingmapandflatMapto create more complex programs. - Executing
IOactions 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
Lazycomputation 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
mapandflatMapto 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
Listmonad represents non-deterministic computation, where an operation can produce multiple results. - Demonstrates:
- Wrapping a
Listinto aKind<ListKind.Witness, A>. - Using
mapto transform every element in the list. - Using
flatMapto 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
Maybemonad provides a way to represent computations that may or may not return a value, explicitly handling the absence of a value. - Demonstrates:
- Creating
JustandNothinginstances. - Using
mapto transform aJustvalue. - Using
flatMapto chain operations that return aMaybe. - Handling the
Nothingcase 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
Optionalmonad provides a way to represent computations that may or may not return a value. - Demonstrates:
- Wrapping
Optionalinstances into aKind<OptionalKind.Witness, A>. - Using
mapto transform the value inside a presentOptional. - Using
flatMapto chain operations that returnOptional. - Using
handleErrorWithto provide a default value when theOptionalis 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 configurationRto produce a valueA, without explicitly passing the configuration object everywhere. - Demonstrates:
- Creating
Readercomputations that access parts of a configuration object. - Using
flatMapto chain computations where one step depends on the result of a previous step and the shared configuration. - Running the final
Readercomputation 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.modifyto update the state andState.inspectto read from it. - Composing these actions into a larger workflow using a
Forcomprehension. - 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
Tryrepresents a computation that results in either aSuccesscontaining a value or aFailurecontaining an exception. - Demonstrates:
- Creating
Tryinstances for successful and failed computations. - Using
mapandflatMapto chain operations, where exceptions are caught and wrapped in aFailure. - Using
recoverandrecoverWithto 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) behaviour.
- Key Concept:
Validatedis used for validation scenarios where you want either to get a valid result or to accumulate validation errors. - Demonstrates:
- Creating
ValidandInvalidinstances. - Using
flatMapto chain validation steps, where the firstInvalidresult short-circuits the computation. - Using
handleErrorWithto 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 resultAand an accumulated valueW(like a log), whereWmust have aMonoidinstance to define how values are combined. - Demonstrates:
- Using
tellto append to the log. - Using
flatMapto sequence computations, where both the results and logs are combined automatically. - Running the final
Writerto 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 aFunctorinstance. - Demonstrates:
- Writing a generic
mapWithFunctorfunction 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);
ProfunctorExample.java
This example demonstrates the Profunctor type class using FunctionProfunctor, showing how to build flexible, adaptable data transformation pipelines.
- Key Concept: A
Profunctoris contravariant in its first parameter and covariant in its second, making it perfect for adapting both the input and output of functions. - Demonstrates:
- Using
lmapto adapt function inputs (contravariant mapping) - Using
rmapto adapt function outputs (covariant mapping) - Using
dimapto adapt both input and output simultaneously - Building real-world API adapters and validation pipelines
- Creating reusable transformation chains
- Using
// Original function: String length calculator
Function<String, Integer> stringLength = String::length;
// Adapt the input: now works with integers!
Kind2<FunctionKind.Witness, Integer, Integer> intToLength =
profunctor.lmap(Object::toString, lengthFunction);
// Adapt the output: now returns formatted strings!
Kind2<FunctionKind.Witness, String, String> lengthToString =
profunctor.rmap(len -> "Length: " + len, lengthFunction);
// Adapt both input and output in one operation
Kind2<FunctionKind.Witness, Integer, String> fullTransform =
profunctor.dimap(Object::toString, len -> "Result: " + len, lengthFunction);
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:
EitherTstacks theEithermonad on top of another monadF, creating a new monadEitherT<F, L, R>that handles both the effects ofFand the failure logic ofEither. - Scenario: Composing synchronous validation (
Either) with an asynchronous operation (CompletableFuture) in a single, clean workflow.
MaybeTExample.java
- Key Concept:
MaybeTstacks theMaybemonad on top of another monadF. This is useful for asynchronous operations that may not return a value. - Scenario: Fetching a userLogin and their preferences from a database asynchronously, where each step might not find a result.
OptionalTExample.java
- Key Concept:
OptionalTstacksOptionalon top of another monadF, creatingOptionalT<F, A>to handle asynchronous operations that may return an empty result. - Scenario: Fetching a userLogin and their preferences from a database asynchronously, where each step might not find a result.
ReaderTExample.java, ReaderTUnitExample.java, ReaderTAsyncUnitExample.java
- Key Concept:
ReaderTcombines theReadermonad (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:
StateTcombines theStatemonad with an outer monadF. This is for stateful computations that also involve effects fromF. - Scenario: A stateful stack that can fail (using
Optionalas the outer monad), where popping from an empty stack results inOptional.empty().
For more advanced patterns combining State with other monads, see the Order Processing Example which demonstrates StateT with EitherT.