Core API Interfaces: The Building Blocks
The hkj-api
module contains the heart of the higher-kinded-j
library—a set of interfaces that define the core functional programming abstractions. These are the building blocks you will use to write powerful, generic, and type-safe code.
This document provides a high-level overview of the most important interfaces, which are often referred to as type classes.
Core HKT Abstraction
At the very centre of the library is the Kind
interface, which makes 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.
The Monad Hierarchy
The most commonly used type classes form a hierarchy of power and functionality, starting with Functor
and building up to Monad
.
Functor<F>
A Functor
is a type class for any data structure that can be "mapped over". It provides a single operation, map
, which applies a function to the value(s) inside the structure without changing the structure itself.
- Key Method:
map(Function<A, B> f, Kind<F, A> fa)
- Intuition: If you have a
List<A>
and a functionA -> B
, aFunctor
forList
lets you produce aList<B>
. The same logic applies toOptional
,Either
,Try
, etc.
Applicative<F>
An Applicative
(or Applicative Functor) is a Functor
with more power. It allows you to apply a function that is itself wrapped in the data structure. This is essential for combining multiple independent computations.
- Key Methods:
of(A value)
: Lifts a normal valueA
into the applicative contextF<A>
.ap(Kind<F, Function<A, B>> ff, Kind<F, A> fa)
: Applies a wrapped function to a wrapped value.
- Intuition: If you have an
Optional<Function<A, B>>
and anOptional<A>
, you can use theApplicative
forOptional
to get anOptional<B>
. This is howValidated
is able to accumulate errors from multiple independent validation steps.
Monad<F>
A Monad
is an Applicative
that adds the power of sequencing dependent computations. It provides a way to chain operations together, where the result of one operation is fed into the next.
- Key Method:
flatMap(Function<A, Kind<F, B>> f, Kind<F, A> fa)
- Intuition:
flatMap
is the powerhouse of monadic composition. It takes a value from a context (like anOptional<A>
), applies a function that returns a new context (A -> Optional<B>
), and flattens the result into a single context (Optional<B>
). This is what enables the elegant, chainable workflows you see in the examples.
MonadError<F, E>
A MonadError
is a specialised Monad
that has a defined error type E
. It provides explicit methods for raising and handling errors within a monadic workflow.
- Key Methods:
raiseError(E error)
: Lifts an errorE
into the monadic contextF<A>
.handleErrorWith(Kind<F, A> fa, Function<E, Kind<F, A>> f)
: Provides a way to recover from a failed computation.
Data Aggregation Type Classes
These type classes define how data can be combined and reduced.
Semigroup<A>
A Semigroup
is a simple type class for any type A
that has an associative combine
operation. It's the foundation for any kind of data aggregation.
- Key Method:
combine(A a1, A a2)
- Use Case: Its primary use in this library is to tell a
Validated``Applicative
how to accumulate errors.
Monoid<A>
A Monoid
is a Semigroup
that also has an "empty" or "identity" element. This is a value that, when combined with any other value, does nothing.
- Key Methods:
combine(A a1, A a2)
(fromSemigroup
)empty()
- Use Case: Essential for folding data structures, where
empty()
provides the starting value for the reduction.
Structure-Iterating Type Classes
These type classes define how to iterate over and manipulate the contents of a data structure in a generic way.
Foldable<F>
A Foldable
is a type class for any data structure F
that can be reduced to a single summary value. It uses a Monoid
to combine the elements.
- Key Method:
foldMap(Monoid<M> monoid, Function<A, M> f, Kind<F, A> fa)
- Intuition: It abstracts the process of iterating over a collection and aggregating the results.
Traverse<F>
A Traverse
is a powerful type class that extends both Functor
and Foldable
. It allows you to iterate over a data structure F<A>
and apply an effectful function A -> G<B>
at each step, collecting the results into a single effect G<F<B>>
.
- Key Method:
traverse(Applicative<G> applicative, Function<A, Kind<G, B>> f, Kind<F, A> fa)
- Use Case: This is incredibly useful for tasks like validating every item in a
List
, where the validation returns aValidated
. The result is a singleValidated
containing either aList
of all successful results or an accumulation of all errors.
Dual-Parameter Type Classes
These type classes work with types that take two type parameters, such as functions, profunctors, and bifunctors.
Profunctor<P>
A Profunctor
is a type class for any type constructor P<A, B>
that is contravariant in its first parameter and covariant in its second. This is the abstraction behind functions and many data transformation patterns.
- Key Methods:
lmap(Function<C, A> f, Kind2<P, A, B> pab)
: Pre-process the input (contravariant mapping)rmap(Function<B, C> g, Kind2<P, A, B> pab)
: Post-process the output (covariant mapping)dimap(Function<C, A> f, Function<B, D> g, Kind2<P, A, B> pab)
: Transform both input and output simultaneously
- Use Case: Essential for building flexible data transformation pipelines, API adapters, and validation frameworks that can adapt to different input and output formats without changing core business logic.
Profunctors in Optics
Importantly, every optic in higher-kinded-j is fundamentally a profunctor. This means that Lens
, Prism
, Iso
, and Traversal
all support profunctor operations through their contramap
, map
, and dimap
methods. This provides incredible flexibility for adapting optics to work with different data types and structures, making them highly reusable across different contexts and API boundaries.