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.
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
Kind
instances, 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
),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
);
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
null
tonarrow
. For external types using aHolder
(likeOptional
withOptionalHolder
), if theKind
instance is not the expectedHolder
type, an exception is also thrown. For types that directly implement theirXxxKind
interface,narrow
will throw if theKind
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.
// 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() {
// 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
}