Usage Guide: Working with Higher-Kinded-J
This guide explains the step-by-step process of using Higher-Kinded-J's simulated Higher-Kinded Types (HKTs) and associated type classes like Functor
, Applicative
, Monad
, and MonadError
.
Core Workflow
The general process involves these steps:
Determine which type constructor (computational context) you want to work with abstractly. This context is represented by its witness type. Examples:
ListKind.Witness
forjava.util.List
OptionalKind.Witness
forjava.util.Optional
MaybeKind.Witness
for the customMaybe
typeEitherKind.Witness<L>
for the customEither<L, R>
type (whereL
is fixed)TryKind.Witness
for the customTry
typeCompletableFutureKind.Witness
forjava.util.concurrent.CompletableFuture
IOKind.Witness
for the customIO
typeLazyKind.Witness
for the customLazy
typeReaderKind.Witness<R_ENV>
for the customReader<R_ENV, A>
typeStateKind.Witness<S>
for the customState<S, A>
typeWriterKind.Witness<W>
for the customWriter<W, A>
type- For transformers, e.g.,
EitherTKind.Witness<F_OUTER_WITNESS, L_ERROR>
Obtain an instance of the required type class (Functor<F_WITNESS>
, Applicative<F_WITNESS>
, Monad<F_WITNESS>
, MonadError<F_WITNESS, E>
) for your chosen context's witness type F_WITNESS
. These are concrete classes provided in the corresponding package.
- Example (
Optional
):OptionalMonad optionalMonad = OptionalMonad.INSTANCE;
(This implementsMonadError<OptionalKind.Witness, Unit>
) - Example (
List
):ListMonad listMonad = new ListMonad();
(This implementsMonad<ListKind.Witness>
) - Example (
CompletableFuture
):CompletableFutureMonad futureMonad = CompletableFutureMonad.INSTANCE;
(This implementsMonadError<CompletableFutureKind.Witness, Throwable>
) - Example (
Either<String, ?>
):EitherMonad<String> eitherMonad = new EitherMonad<>();
(This implementsMonadError<EitherKind.Witness<String>, String>
) - Example (
IO
):IOMonad ioMonad = IOMonad.INSTANCE;
(This implementsMonad<IOKind.Witness>
) - Example (
Writer<String, ?>
):WriterMonad<String> writerMonad = new WriterMonad<>(new StringMonoid());
(This implementsMonad<WriterKind.Witness<String>>
)
Convert your standard Java object (e.g., a List<Integer>
, an Optional<String>
, an IO<String>
) into Higher-Kinded-J's Kind
representation using the widen
instance method from the corresponding XxxKindHelper
enum's singleton instance. You'll typically use a static import for the singleton instance for brevity.
import static org.higherkindedj.hkt.optional.OptionalKindHelper.OPTIONAL; // Static import
// ...
Optional<String> myOptional = Optional.of("test");
// Widen it into the Higher-Kinded-J Kind type
// F_WITNESS here is OptionalKind.Witness
Kind<OptionalKind.Witness, String> optionalKind = OPTIONAL.widen(myOptional);
- Helper enums provide convenience factory methods that also return
Kind
instances, e.g.,MaybeKindHelper.MAYBE.just("value")
,TryKindHelper.TRY.failure(ex)
,IOKindHelper.IO_OP.delay(() -> ...)
,LazyKindHelper.LAZY.defer(() -> ...)
. Use these when appropriate (assumingMAYBE
,TRY
,IO_OP
,LAZY
are the respective singleton constant names). - Note on Widening:
- For JDK types (like
List
,Optional
),widen
typically creates an internalHolder
object that wraps the JDK type and implements the necessaryXxxKind
interface. - For library-defined types (
Id
,Maybe
,IO
, Transformers likeEitherT
) that directly implement theirXxxKind
interface (which in turn extendsKind
), thewiden
method on the helper enum often performs a null check and then a direct (and safe) cast to theKind
type.
- For JDK types (like
Use the methods defined by the type class interface (map
, flatMap
, of
, ap
, raiseError
, handleErrorWith
, etc.) by calling them on the type class instance obtained in Step 2, passing your Kind
value(s) as arguments. Do not call map
/flatMap
directly on the Kind
object itself if it's just the Kind
interface. (Some concrete Kind
implementations like Id
or Maybe
might offer direct methods, but for generic programming, use the type class instance).
import static org.higherkindedj.hkt.optional.OptionalKindHelper.OPTIONAL;
// ...
OptionalMonad optionalMonad = OptionalMonad.INSTANCE;
Kind<OptionalKind.Witness, String> optionalKind = OPTIONAL.widen(Optional.of("test")); // from previous step
// --- Using map ---
Function<String, Integer> lengthFunc = String::length;
// Apply map using the monad instance
Kind<OptionalKind.Witness, Integer> lengthKind = optionalMonad.map(lengthFunc, optionalKind);
// lengthKind now represents Kind<OptionalKind.Witness, Integer> containing Optional.of(4) if "test"
// --- Using flatMap ---
// Function A -> Kind<F_WITNESS, B>
Function<Integer, Kind<OptionalKind.Witness, String>> checkLength =
len -> OPTIONAL.widen(len > 3 ? Optional.of("Long enough") : Optional.empty());
// Apply flatMap using the monad instance
Kind<OptionalKind.Witness, String> checkedKind = optionalMonad.flatMap(checkLength, lengthKind);
// checkedKind now represents Kind<OptionalKind.Witness, String> containing Optional.of("Long enough")
// --- Using MonadError (for Optional, error type is Unit) ---
Kind<OptionalKind.Witness, String> emptyKind = optionalMonad.raiseError(Unit.INSTANCE); // Represents Optional.empty()
// Handle the empty case (error state) using handleErrorWith (handleError was changed to handleErrorWith generally)
Kind<OptionalKind.Witness, String> handledKind = optionalMonad.handleErrorWith(
emptyKind,
ignoredError -> OPTIONAL.widen(Optional.of("Default Value")) // Ensure recovery function also returns a Kind
);
</div>
</div>
<div id="admonition-5-unwrapnarrow-the-result-_kind---javatype_" class="admonition admonish-note" role="note" aria-labelledby="admonition-5-unwrapnarrow-the-result-_kind---javatype_-title">
<div class="admonition-title">
<div id="admonition-5-unwrapnarrow-the-result-_kind---javatype_-title">
5: Unwrap/Narrow the Result (_Kind<F_WITNESS, A> -> JavaType<A>_)
</div>
<a class="admonition-anchor-link" href="#admonition-5-unwrapnarrow-the-result-_kind---javatype_"></a>
</div>
<div>
- [GenericExample.java](https://github.com/higher-kinded-j/higher-kinded-j/tree/main/src/main/java/org/higherkindedj/example/basic/GenericExample.java)
WWhen you need the underlying Java value back (e.g., to return from a method boundary, perform side effects like printing or running `IO`), use the `narrow` instance method from the corresponding `XxxKindHelper` enum's singleton instance.
```java
import static org.higherkindedj.hkt.optional.OptionalKindHelper.OPTIONAL;
import static org.higherkindedj.hkt.io.IOKindHelper.IO_OP;
// ...
// Continuing the Optional example:
Kind<OptionalKind.Witness, String> checkedKind = /* from previous step */;
Kind<OptionalKind.Witness, String> handledKind = /* from previous step */;
Optional<String> finalOptional = OPTIONAL.narrow(checkedKind);
System.out.println("Final Optional: " + finalOptional); // Output: Optional[Long enough]
Optional<String> handledOptional = OPTIONAL.narrow(handledKind);
System.out.println("Handled Optional: " + handledOptional); // Output: Optional[Default Value]
// Example for IO:
IOMonad ioMonad = IOMonad.INSTANCE;
Kind<IOKind.Witness, String> ioKind = IO_OP.delay(() -> "Hello from IO!"); // Use IO_OP.delay
// unsafeRunSync is an instance method on IOKindHelper.IO_OP
String ioResult = IO_OP.unsafeRunSync(ioKind);
System.out.println(ioResult);
```
</div>
</div>
-----
<div id="admonition-handling-_kindunwrapexception_" class="admonition admonish-note" role="note" aria-labelledby="admonition-handling-_kindunwrapexception_-title">
<div class="admonition-title">
<div id="admonition-handling-_kindunwrapexception_-title">
Handling _KindUnwrapException_
</div>
<a class="admonition-anchor-link" href="#admonition-handling-_kindunwrapexception_"></a>
</div>
<div>
- [GenericExample.java](https://github.com/higher-kinded-j/higher-kinded-j/tree/main/src/main/java/org/higherkindedj/example/basic/GenericExample.java)
The `narrow` instance methods in all `KindHelper` enums are designed to be robust against *structural* errors within the HKT simulation layer.
* **When it's thrown**: If you pass `null` to `narrow`. For external types using a `Holder` (like `Optional` with `OptionalHolder`), if the `Kind` instance is not the expected `Holder` type, an exception is also thrown. For types that directly implement their `XxxKind` interface, `narrow` will throw if the `Kind` is not an instance of that specific concrete type.
* **What it means**: This exception signals a problem with how you are using Higher-Kinded-J itself – usually a programming error in creating or passing `Kind` objects.
* **How to handle**: You generally **should not** need to catch `KindUnwrapException` in typical application logic. Its occurrence points to a bug that needs fixing in the code using Higher-Kinded-J.
```java
// import static org.higherkindedj.hkt.optional.OptionalKindHelper.OPTIONAL;
public void handlingUnwrapExceptions() {
try {
// ERROR: Attempting to narrow null
Optional<String> result = OPTIONAL.narrow(null);
} catch(KindUnwrapException e) {
System.err.println("Higher-Kinded-J Usage Error: " + e.getMessage());
// Example Output (message from OptionalKindHelper.INVALID_KIND_NULL_MSG):
// Usage Error: Cannot narrow null Kind for Optional
}
}
Important Distinction:
KindUnwrapException
: Signals a problem with the Higher-Kinded-J structure itself (e.g., invalidKind
object passed tonarrow
). Fix the code using Higher-Kinded-J.- Domain Errors / Absence: Represented within a valid
Kind
structure (e.g.,Optional.empty()
widened toKind<OptionalKind.Witness, A>
,Either.Left
widened toKind<EitherKind.Witness<L>, R>
). These should be handled using the monad's specific methods (orElse
,fold
,handleErrorWith
, etc.) or by using theMonadError
methods before narrowing back to the final Java type.
Higher-Kinded-J allows writing functions generic over the simulated type constructor (represented by its witness F_WITNESS
).
// import static org.higherkindedj.hkt.list.ListKindHelper.LIST;
// import static org.higherkindedj.hkt.optional.OptionalKindHelper.OPTIONAL;
// ...
// Generic function: Applies a function within any Functor context F_WITNESS.
// Requires the specific Functor<F_WITNESS> instance to be passed in.
public static <F_WITNESS, A, B> Kind<F_WITNESS, B> mapWithFunctor(
Functor<F_WITNESS> functorInstance, // Pass the type class instance for F_WITNESS
Function<A, B> fn,
Kind<F_WITNESS, A> kindABox) {
// Use the map method from the provided Functor instance
return functorInstance.map(fn, kindABox);
}
public void genericExample() { // Corrected typo from genricExample
// Get instances of the type classes for the specific types (F_WITNESS) we want to use
ListMonad listMonad = new ListMonad(); // Implements Functor<ListKind.Witness>
OptionalMonad optionalMonad = OptionalMonad.INSTANCE; // Implements Functor<OptionalKind.Witness>
Function<Integer, Integer> doubleFn = x -> x * 2;
// --- Use with List ---
List<Integer> nums = List.of(1, 2, 3);
// Widen the List. F_WITNESS is ListKind.Witness
Kind<ListKind.Witness, Integer> listKind = LIST.widen(nums);
// Call the generic function, passing the ListMonad instance and the widened List
Kind<ListKind.Witness, Integer> doubledListKind = mapWithFunctor(listMonad, doubleFn, listKind);
System.out.println("Doubled List: " + LIST.narrow(doubledListKind)); // Output: [2, 4, 6]
// --- Use with Optional (Present) ---
Optional<Integer> optNum = Optional.of(10);
// Widen the Optional. F_WITNESS is OptionalKind.Witness
Kind<OptionalKind.Witness, Integer> optKind = OPTIONAL.widen(optNum);
// Call the generic function, passing the OptionalMonad instance and the widened Optional
Kind<OptionalKind.Witness, Integer> doubledOptKind = mapWithFunctor(optionalMonad, doubleFn, optKind);
System.out.println("Doubled Optional: " + OPTIONAL.narrow(doubledOptKind)); // Output: Optional[20]
// --- Use with Optional (Empty) ---
Optional<Integer> emptyOpt = Optional.empty();
Kind<OptionalKind.Witness, Integer> emptyOptKind = OPTIONAL.widen(emptyOpt);
// Call the generic function, map does nothing on empty
Kind<OptionalKind.Witness, Integer> doubledEmptyOptKind = mapWithFunctor(optionalMonad, doubleFn, emptyOptKind);
System.out.println("Doubled Empty Optional: " + OPTIONAL.narrow(doubledEmptyOptKind)); // Output: Optional.empty
}