Core Concepts of Higher-Kinded-J
- How the Kind<F, A> interface simulates higher-kinded types in Java
- The role of witness types in representing type constructors
- Understanding defunctionalisation and how it enables HKT simulation
- The difference between internal library types and external Java types
- How type classes provide generic operations across different container types
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.
Feel free to skip ahead to the examples and come back later for the theory
1. The HKT Problem in Java
As we have already discussed, Java's type system lacks native Higher-Kinded Types. We can easily parametrise a type by another type (like List<String>
), but we cannot easily parametrise 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".
You will often see Higher-Kinded Types represented with an underscore, like F<_>
(e.g., List<_>
, Optional<_>
). This notation, borrowed from languages like Scala, represents a "type constructor"—a type that is waiting for a type parameter. It's important to note that this underscore is a conceptual placeholder and is not the same as Java's ?
wildcard, which is used for instantiated types. Our library provides a way to simulate this F<_>
concept in Java.
2. The Kind<F, A>
Bridge
At the very centre of the library are the Kind
interfaces, which make higher-kinded types possible in Java.
-
Kind<F, A>
: This is the foundational interface that emulates a higher-kinded type. It represents a typeF
that is generic over a typeA
. For example,Kind<ListKind.Witness, String>
represents aList<String>
. You will see this interface used everywhere as the common currency for all our functional abstractions. -
Kind2<F, A, B>
: This interface extends the concept to types that take two type parameters, such asFunction<A, B>
orEither<L, R>
. For example,Kind2<FunctionKind.Witness, String, Integer>
represents aFunction<String, Integer>
. This is essential for working with profunctors and other dual-parameter abstractions.
- 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 parametrised by the constructor (e.g.,Integer
inList<Integer>
).- How it Works: The library provides a seamless bridge between a standard java type, like a
java.util.List<Integer>
and itsKind
representationKind<ListKind.Witness, Integer>
. Instead of requiring you to manually wrap objects, this conversion is handled by static helper methods, typicallywiden
andnarrow
.- To treat a
List<Integer>
as aKind
, you use a helper function likeLIST.widen()
. - This
Kind
object can then be passed to generic functions (such asmap
orflatMap
from aFunctor
orMonad
instance) that expectKind<F, A>
.
- To treat 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.