Usage Guide: Working with Higher-Kinded-J

- The five-step workflow for using Higher-Kinded-J effectively
- How to identify the right context (witness type) for your use case
- Using widen() and narrow() to convert between Java types and Kind representations
- When and how to handle KindUnwrapException safely
- Writing generic functions that work with any Functor or Monad
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.Witnessforjava.util.ListOptionalKind.Witnessforjava.util.OptionalMaybeKind.Witnessfor the customMaybetypeEitherKind.Witness<L>for the customEither<L, R>type (whereLis fixed)TryKind.Witnessfor the customTrytypeCompletableFutureKind.Witnessforjava.util.concurrent.CompletableFutureIOKind.Witnessfor the customIOtypeLazyKind.Witnessfor the customLazytypeReaderKind.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.
Examples:
Optional:OptionalMonad optionalMonad = OptionalMonad.INSTANCE;(This implementsMonadError<OptionalKind.Witness, Unit>)List:ListMonad listMonad = ListMonad.INSTANCE;(This implementsMonad<ListKind.Witness>)CompletableFuture:CompletableFutureMonad futureMonad = CompletableFutureMonad.INSTANCE;(This implementsMonadError<CompletableFutureKind.Witness, Throwable>)Either<String, ?>:EitherMonad<String> eitherMonad = EitherMonad.instance();(This implementsMonadError<EitherKind.Witness<String>, String>)IO:IOMonad ioMonad = IOMonad.INSTANCE;(This implementsMonad<IOKind.Witness>)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;
// ...
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
Kindinstances, e.g.,MAYBE.just("value"),TRY.failure(ex),IO_OP.delay(() -> ...),LAZY.defer(() -> ...). Remember to import thes statically from the XxxKindHelper classes. - Note on Widening:
- For JDK types (like
List,Optional),widentypically creates an internalHolderobject that wraps the JDK type and implements the necessaryXxxKindinterface. - For library-defined types (
Id,IO,Maybe,Either,Validated, Transformers likeEitherT) that directly implement theirXxxKindinterface (which in turn extendsKind), thewidenmethod on the helper enum performs a null check and then a direct (and safe) cast to theKindtype. This provides zero runtime overhead—no wrapper object allocation needed.
- 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)
// --- 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
Kind<OptionalKind.Witness, String> handledKind = optionalMonad.handleErrorWith(
emptyKind,
ignoredError -> OPTIONAL.widen(Optional.of("Default Value")) // Ensure recovery function also returns a Kind
);
Note: For complex chains of monadic operations, consider using For Comprehensions which provide more readable syntax than nested flatMap calls.
When 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);
```
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
nulltonarrow. For external types using aHolder(likeOptionalwithOptionalHolder), if theKindinstance is not the expectedHoldertype, an exception is also thrown. For types that directly implement theirXxxKindinterface,narrowwill throw if theKindis 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
Kindobjects. - How to handle: You generally should not need to catch
KindUnwrapExceptionin typical application logic. Its occurrence points to a bug that needs fixing in the code using Higher-Kinded-J.
// 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., invalidKindobject passed tonarrow). Fix the code using Higher-Kinded-J.- Domain Errors / Absence: Represented within a valid
Kindstructure (e.g.,Optional.empty()widened toKind<OptionalKind.Witness, A>,Either.Leftwidened toKind<EitherKind.Witness<L>, R>). These should be handled using the monad's specific methods (orElse,fold,handleErrorWith, etc.) or by using theMonadErrormethods 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() {
// 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
}