Core Concepts of Higher-Kinded-J
Higher-Kinded-J employs several key components to emulate Higher-Kinded Types (HKTs) and associated functional type classes in Java. Understanding these is crucial for using and extending the library.
1. The HKT Problem in Java
Java's type system lacks native Higher-Kinded Types. We can easily parameterise a type by another type (like List<String>
), but we cannot easily parameterise a type or method by a type constructor itself (like F<_>
). We can't write void process<F<_>>(F<Integer> data)
to mean "process any container F of Integers".
2. The Kind<F, A>
Bridge
- Purpose: To simulate the application of a type constructor
F
(likeList
,Optional
,IO
) to a type argumentA
(likeString
,Integer
), representing the concept ofF<A>
. F
(Witness Type): This is the crucial part of the simulation. SinceF<_>
isn't a real Java type parameter, we use a marker type (often an empty interface specific to the constructor) as a "witness" or stand-in forF
. Examples:ListKind<ListKind.Witness>
represents theList
type constructor.OptionalKind<OptionalKind.Witness>
represents theOptional
type constructor.EitherKind.Witness<L>
represents theEither<L, _>
type constructor (whereL
is fixed).IOKind<IOKind.Witness>
represents theIO
type constructor.
A
(Type Argument): The concrete type contained within or parameterised by the constructor (e.g.,Integer
inList<Integer>
).- How it Works: An actual object, like a
java.util.List<Integer>
, is wrapped in a helper class (e.g.,ListHolder
) which implementsKind<ListKind<?>, Integer>
. ThisKind
object can then be passed to generic functions that expectKind<F, A>
. - Reference:
Kind.java
3. Type Classes (Functor
, Applicative
, Monad
, MonadError
)
These are interfaces that define standard functional operations that work generically over any simulated type constructor F
(represented by its witness type) for which an instance of the type class exists. They operate on Kind<F, A>
objects.
Functor<F>
:- Defines
map(Function<A, B> f, Kind<F, A> fa)
: Applies a functionf: A -> B
to the value(s) inside the contextF
without changing the context's structure, resulting in aKind<F, B>
. ThinkList.map
,Optional.map
. - Laws: Identity (
map(id) == id
), Composition (map(g.compose(f)) == map(g).compose(map(f))
). - Reference:
Functor.java
- Defines
Applicative<F>
:- Extends
Functor<F>
. - Adds
of(A value)
: Lifts a pure valueA
into the contextF
, creating aKind<F, A>
. (e.g.,1
becomesOptional.of(1)
wrapped inKind
). - Adds
ap(Kind<F, Function<A, B>> ff, Kind<F, A> fa)
: Applies a function wrapped in contextF
to a value wrapped in contextF
, returning aKind<F, B>
. This enables combining multiple independent values within the context. - Provides default
mapN
methods (e.g.,map2
,map3
) built uponap
andmap
. - Laws: Identity, Homomorphism, Interchange, Composition.
- Reference:
Applicative.java
- Extends
Monad<F>
:- Extends
Applicative<F>
. - Adds
flatMap(Function<A, Kind<F, B>> f, Kind<F, A> ma)
: Sequences operations within the contextF
. Takes a valueA
from contextF
, applies a functionf
that returns a new contextKind<F, B>
, and returns the result flattened into a singleKind<F, B>
. Essential for chaining dependent computations (e.g., chainingOptional
calls, sequencingCompletableFuture
s, combiningIO
actions). Also known in functional languages asbind
or>>=
. - Laws: Left Identity, Right Identity, Associativity.
- Reference:
Monad.java
- Extends
MonadError<F, E>
:- Extends
Monad<F>
. - Adds error handling capabilities for contexts
F
that have a defined error typeE
. - Adds
raiseError(E error)
: Lifts an errorE
into the contextF
, creating aKind<F, A>
representing the error state (e.g.,Either.Left
,Try.Failure
or failedCompletableFuture
). - Adds
handleErrorWith(Kind<F, A> ma, Function<E, Kind<F, A>> handler)
: Allows recovering from an error stateE
by providing a function that takes the error and returns a new contextKind<F, A>
. - Provides default recovery methods like
handleError
,recover
,recoverWith
. - Reference:
MonadError.java
- Extends
4. Defunctionalisation (Per Type Constructor)
For each Java type constructor (like List
, Optional
, IO
) you want to simulate as a Higher-Kinded Type, a specific pattern involving several components is used. The exact implementation differs slightly depending on whether the type is defined within the Higher-Kinded-J library (e.g., Id
, Maybe
, IO
, monad transformers) or if it's an external type (e.g., java.util.List
, java.util.Optional
, java.util.concurrent.CompletableFuture
).
Common Components:
-
The
XxxKind
Interface: A specific marker interface, for example,OptionalKind<A>
. This interface extendsKind<F, A>
, whereF
is the witness type representing the type constructor.- Example:
public interface OptionalKind<A> extends Kind<OptionalKind.Witness, A> { /* ... Witness class ... */ }
- The
Witness
(e.g.,OptionalKind.Witness
) is a static nested final class (or a separate accessible class) withinOptionalKind
. ThisWitness
type is what's used as theF
parameter in generic type classes likeMonad<F>
.
- Example:
-
The
KindHelper
Class (e.g.,OptionalKindHelper
): A crucial utilitywiden
andnarrow
methods:widen(...)
: Converts the standard Java type (e.g.,Optional<String>
) into itsKind<F, A>
representation.narrow(Kind<F, A> kind)
: Converts theKind<F, A>
representation back to the underlying Java type (e.g.,Optional<String>
).- Crucially, this method throws
KindUnwrapException
if the inputkind
is structurally invalid (e.g.,null
, the wrongKind
type, or, where applicable, aHolder
containingnull
where it shouldn't). This ensures robustness.
- Crucially, this method throws
- May contain other convenience factory methods.
-
Type Class Instance(s): Concrete classes implementing
Functor<F>
,Monad<F>
, etc., for the specific witness typeF
(e.g.,OptionalMonad implements Monad<OptionalKind.Witness>
). These instances use theKindHelper
'swiden
andnarrow
methods to operate on the underlying Java types.
External Types:
- For Types Defined Within Higher-Kinded-J (e.g.,
Id
,Maybe
,IO
, Monad Transformers likeEitherT
):- These types are designed to directly participate in the HKT simulation.
- The type itself (e.g.,
Id<A>
,MaybeT<F, A>
) will directly implement its correspondingXxxKind
interface (e.g.,Id<A> implements IdKind<A>
, whereIdKind<A> extends Kind<Id.Witness, A>
). - In this case, a separate
Holder
record is not needed for the primarywrap
/unwrap
mechanism in theKindHelper
. XxxKindHelper.wrap(Id<A> id)
would effectively be a type cast (after null checks) toKind<Id.Witness, A>
becauseId<A>
is already anIdKind<A>
.XxxKindHelper.unwrap(Kind<Id.Witness, A> kind)
would checkinstanceof Id
(orinstanceof MaybeT
, etc.) and perform a cast.
This distinction is important for understanding how wrap
and unwrap
function for different types. However, from the perspective of a user of a type class instance (like OptionalMonad
), the interaction remains consistent: you provide a Kind
object, and the type class instance handles the necessary operations.
5. The Unit
Type
In functional programming, it's common to have computations or functions that perform an action (often a side effect) but do not produce a specific, meaningful result value. In Java, methods that don't return a value use the void
keyword. However, void
is not a first-class type and cannot be used as a generic type parameter A
in Kind<F, A>
.
Higher-Kinded-J provides the org.higherkindedj.hkt.unit.Unit
type to address this.
- Purpose:
Unit
is a type that has exactly one value,Unit.INSTANCE
. It is used to represent the successful completion of an operation that doesn't yield any other specific information. Think of it as a functional equivalent ofvoid
, but usable as a generic type. - Usage in HKT:
- When a monadic action
Kind<F, A>
completes successfully but has no specific value to return (e.g., anIO
action that prints to the console),A
can beUnit
. The action would then beKind<F, Unit>
, and its successful result would conceptually beUnit.INSTANCE
. For example,IO<Unit>
for a print operation. - In
MonadError<F, E>
, if the error stateE
simply represents an absence or a failure without specific details (likeOptional.empty()
orMaybe.Nothing()
),Unit
can be used as the type forE
. TheraiseError
method would then be called withUnit.INSTANCE
. For instance,OptionalMonad
implementsMonadError<OptionalKind.Witness, Unit>
, andMaybeMonad
implementsMonadError<MaybeKind.Witness, Unit>
.
- When a monadic action
- Example:
// An IO action that just performs a side effect (printing) Kind<IOKind.Witness, Unit> printAction = IOKindHelper.delay(() -> { System.out.println("Effect executed!"); return Unit.INSTANCE; // Explicitly return Unit.INSTANCE }); IOKindHelper.unsafeRunSync(printAction); // Executes the print // Optional treated as MonadError<..., Unit> OptionalMonad optionalMonad = OptionalMonad.INSTANCE; Kind<OptionalKind.Witness, String> emptyOptional = optionalMonad.raiseError(Unit.INSTANCE); // Creates Optional.empty()
- Reference:
Unit.java
6. Error Handling Philosophy
- Domain Errors: These are expected business-level errors or alternative outcomes. They are represented within the structure of the simulated type (e.g.,
Either.Left
,Maybe.Nothing
,Try.Failure
, a failedCompletableFuture
, potentially a specific result type withinIO
). These are handled using the type's specific methods orMonadError
capabilities (handleErrorWith
,recover
,fold
,orElse
, etc.) after successfully unwrapping theKind
. - Simulation Errors (
KindUnwrapException
): These indicate a problem with the HKT simulation itself – usually a programming error. Examples include passingnull
tounwrap
, passing aListKind
toOptionalKindHelper.unwrap
, or (if it were possible) having aHolder
record contain anull
reference to the underlying Java object it's supposed to hold. These are signalled by throwing the uncheckedKindUnwrapException
fromunwrap
methods to clearly distinguish infrastructure issues from domain errors. You typically shouldn't need to catchKindUnwrapException
unless debugging the simulation usage itself.