Quick Reference Guide
This section provides at-a-glance summaries of all type classes in Higher-Kinded-J. Use this as a quick lookup while coding or to compare different type classes.
Core Type Classes
Functor
Core Method: map(Function<A,B> f, Kind<F,A> fa) -> Kind<F,B>
Purpose: Transform values inside a context without changing the context structure
Use When:
- You have a simple transformation function
A -> B
- The context/container should remain unchanged
- No dependency between input and output contexts
Laws:
- Identity:
map(identity) == identity
- Composition:
map(g ∘ f) == map(g) ∘ map(f)
Common Instances: List, Optional, Maybe, Either, IO, CompletableFuture
Example:
// Transform string to length, preserving Optional context
Kind<OptionalKind.Witness, Integer> lengths =
optionalFunctor.map(String::length, optionalString);
Think Of It As: Applying a function "inside the box" without opening it
Applicative
Core Methods:
of(A value) -> Kind<F,A>
(lift pure value)ap(Kind<F,Function<A,B>> ff, Kind<F,A> fa) -> Kind<F,B>
(apply wrapped function)
Purpose: Combine independent computations within a context
Use When:
- You need to combine multiple wrapped values
- Operations are independent (don't depend on each other's results)
- You want to accumulate errors from multiple validations
Key Insight: map2
, map3
, etc. are built on ap
for combining 2, 3, or more values
Laws: Identity, Composition, Homomorphism, Interchange
Common Patterns:
- Form validation (collect all errors)
- Combining configuration values
- Parallel computations
Example:
// Combine two independent validations
Kind<ValidatedKind.Witness<List<String>>, User> user =
applicative.map2(
validateUsername(input.username()),
validatePassword(input.password()),
User::new
);
Think Of It As: Combining multiple "boxes" when contents are independent
Monad
Core Method: flatMap(Function<A,Kind<F,B>> f, Kind<F,A> fa) -> Kind<F,B>
Purpose: Sequence dependent computations within a context
Use When:
- Each step depends on the result of the previous step
- You need to chain operations that return wrapped values
- You want short-circuiting behaviour on failure
Key Difference from Applicative: Operations are sequential and dependent
Laws:
- Left Identity:
flatMap(f, of(a)) == f(a)
- Right Identity:
flatMap(of, m) == m
- Associativity:
flatMap(g, flatMap(f, m)) == flatMap(x -> flatMap(g, f(x)), m)
Utility Methods:
as(B value, Kind<F,A> fa)
- replace value, keep effectpeek(Consumer<A> action, Kind<F,A> fa)
- side effect without changing value
Example:
// Chain database operations where each depends on the previous
Kind<OptionalKind.Witness, Account> account =
monad.flatMap(user ->
monad.flatMap(profile ->
findAccount(profile.accountId()),
findProfile(user.id())),
findUser(userId));
Think Of It As: Chaining operations where each "opens the box" and "puts result in new box"
MonadError
Core Methods:
raiseError(E error) -> Kind<F,A>
(create error state)handleErrorWith(Kind<F,A> fa, Function<E,Kind<F,A>> handler) -> Kind<F,A>
(recover from error)
Purpose: Add explicit error handling to monadic computations
Use When:
- You need to handle specific error types
- You want to recover from failures in a workflow
- You need to distinguish between different kinds of failures
Key Insight: Error type E
is fixed for each MonadError instance
Common Error Types:
Throwable
for CompletableFutureUnit
for Optional/Maybe (absence as error)- Custom domain error types for Either/Validated
Recovery Methods:
handleError(fa, Function<E,A> handler)
- recover to pure valuerecover(fa, A defaultValue)
- provide default value
Example:
// Handle division by zero gracefully
Kind<EitherKind.Witness<String>, Double> result =
monadError.handleErrorWith(
divideOperation,
error -> monadError.of(0.0) // recover with default
);
Think Of It As: try-catch for functional programming
Data Combination Type Classes
Semigroup
Core Method: combine(A a1, A a2) -> A
Purpose: Types that can be combined associatively
Key Property: Associativity - combine(a, combine(b, c)) == combine(combine(a, b), c)
Use When:
- You need to combine/merge two values of the same type
- Order of combination doesn't matter (due to associativity)
- Building blocks for parallel processing
Common Instances:
- String concatenation:
"a" + "b" + "c"
- Integer addition:
1 + 2 + 3
- List concatenation:
[1,2] + [3,4] + [5,6]
- Set union:
{1,2} ∪ {2,3} ∪ {3,4}
Example:
// Combine error messages
Semigroup<String> stringConcat = Semigroups.string("; ");
String combined = stringConcat.combine("Error 1", "Error 2");
// Result: "Error 1; Error 2"
Think Of It As: The +
operator generalised to any type
Monoid
Core Methods:
combine(A a1, A a2) -> A
(from Semigroup)empty() -> A
(identity element)
Purpose: Semigroups with an identity/neutral element
Key Properties:
- Associativity (from Semigroup)
- Identity:
combine(a, empty()) == combine(empty(), a) == a
Use When:
- You need a starting value for reductions/folds
- Implementing fold operations over data structures
- You might be combining zero elements
Common Instances:
- String: empty =
""
, combine = concatenation - Integer addition: empty =
0
, combine =+
- Integer multiplication: empty =
1
, combine =*
- List: empty =
[]
, combine = concatenation - Boolean AND: empty =
true
, combine =&&
Example:
// Sum a list using integer addition monoid
Integer sum = listFoldable.foldMap(
Monoids.integerAddition(),
Function.identity(),
numbersList
);
Think Of It As: Semigroup + a "starting point" for combinations
Structure-Iterating Type Classes
Foldable
Core Method: foldMap(Monoid<M> monoid, Function<A,M> f, Kind<F,A> fa) -> M
Purpose: Reduce a data structure to a single summary value
Use When:
- You want to aggregate/summarise data in a structure
- You need different types of reductions (sum, concat, any/all, etc.)
- You want to count, find totals, or collapse collections
Key Insight: Different Monoids give different aggregations from same data
Common Operations:
- Sum numbers: use integer addition monoid
- Concatenate strings: use string monoid
- Check all conditions: use boolean AND monoid
- Count elements: map to 1, use integer addition monoid
Example:
// Multiple aggregations of the same list
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Sum
Integer sum = foldable.foldMap(Monoids.integerAddition(),
Function.identity(), numbers); // 15
// Concatenate as strings
String concat = foldable.foldMap(Monoids.string(),
String::valueOf, numbers); // "12345"
// Check all positive
Boolean allPositive = foldable.foldMap(Monoids.booleanAnd(),
n -> n > 0, numbers); // true
Think Of It As: Swiss Army knife for data aggregation
Traverse
Core Method: traverse(Applicative<G> app, Function<A,Kind<G,B>> f, Kind<F,A> fa) -> Kind<G,Kind<F,B>>
Purpose: Apply an effectful function to each element and "flip" the contexts
Use When:
- You have a collection and want to apply an effect to each element
- You want to validate every item and collect all errors
- You need to "turn inside-out":
F<G<A>>
becomesG<F<A>>
Key Operations:
traverse
: apply function then flipsequence
: just flip contexts (when you already haveF<G<A>>
)
Common Patterns:
- Validate every item in a list
- Make async calls for each element
- Parse/process each item, collecting all failures
Example:
// Validate every string in a list, collect all errors
List<String> inputs = List.of("123", "abc", "456");
Kind<ValidatedKind.Witness<List<String>>, Kind<ListKind.Witness, Integer>> result =
listTraverse.traverse(
validatedApplicative,
this::parseInteger, // String -> Validated<List<String>, Integer>
LIST.widen(inputs)
);
// Result: either Valid(List[123, 456]) or Invalid(["abc is not a number"])
Think Of It As: Applying effects to collections while flipping the "nesting order"
Dual-Parameter Type Classes
Profunctor
Core Methods:
lmap(Function<C,A> f, Kind2<P,A,B> pab) -> Kind2<P,C,B>
(contravariant on input)rmap(Function<B,D> g, Kind2<P,A,B> pab) -> Kind2<P,A,D>
(covariant on output)dimap(Function<C,A> f, Function<B,D> g, Kind2<P,A,B> pab) -> Kind2<P,C,D>
(both)
Purpose: Adapt inputs and outputs of two-parameter types (especially functions)
Use When:
- Building flexible data transformation pipelines
- Creating API adapters that convert between different formats
- You need to preprocess inputs or postprocess outputs
- Building reusable validation or transformation logic
Key Insight:
lmap
= preprocess the input (contravariant)rmap
= postprocess the output (covariant)dimap
= do both transformations
Common Instance: Function<A,B>
is the canonical Profunctor
Example:
// Adapt a string length function to work with integers and return formatted strings
Function<String, Integer> stringLength = String::length;
// Input adaptation: Integer -> String
Kind2<FunctionKind.Witness, Integer, Integer> intToLength =
profunctor.lmap(Object::toString, FUNCTION.widen(stringLength));
// Output adaptation: Integer -> String
Kind2<FunctionKind.Witness, String, String> lengthToString =
profunctor.rmap(len -> "Length: " + len, FUNCTION.widen(stringLength));
// Both adaptations
Kind2<FunctionKind.Witness, Integer, String> fullAdaptation =
profunctor.dimap(Object::toString, len -> "Result: " + len,
FUNCTION.widen(stringLength));
Think Of It As: The adapter pattern for functional programming
Decision Guide
Start Simple, Go Complex:
- Functor - Simple transformations, context unchanged
- Applicative - Combine independent computations
- Monad - Chain dependent computations
- MonadError - Add error handling to Monad
- Traverse - Apply effects to collections
- Profunctor - Adapt inputs/outputs of functions
Decision Tree:
- Need to transform values? → Functor
- Need to combine independent operations? → Applicative
- Need sequential dependent operations? → Monad
- Need error recovery? → MonadError
- Need to process collections with effects? → Traverse
- Need to adapt function interfaces? → Profunctor
- Need to aggregate/summarise data? → Foldable
- Need to combine values? → Semigroup/Monoid
Common Patterns:
- Form validation: Applicative (independent fields) or Traverse (list of fields)
- Database operations: Monad (dependent queries) + MonadError (failure handling)
- API integration: Profunctor (adapt formats) + Monad (chain calls)
- Configuration: Applicative (combine settings) + Reader (dependency injection)
- Logging: Writer (accumulate logs) + Monad (sequence operations)
- State management: State/StateT (thread state) + Monad (sequence updates)
Type Hierarchy
Functor
↑
Applicative ← Apply
↑
Monad
↑
MonadError
Semigroup
↑
Monoid
Functor + Foldable
↑
Traverse
(Two-parameter types)
Profunctor
Inheritance Meaning:
- Every Applicative is also a Functor
- Every Monad is also an Applicative (and therefore a Functor)
- Every MonadError is also a Monad (and therefore Applicative and Functor)
- Every Monoid is also a Semigroup
- Every Traverse provides both Functor and Foldable capabilities
Practical Implication: If you have a Monad<F>
instance, you can also use it as a Functor<F>
or Applicative<F>
.
Common Monoid Instances
Numeric:
Monoids.integerAddition()
- sum integers (empty = 0)Monoids.integerMultiplication()
- multiply integers (empty = 1)
Text:
Monoids.string()
- concatenate strings (empty = "")Monoids.string(delimiter)
- join with delimiter
Boolean:
Monoids.booleanAnd()
- logical AND (empty = true)Monoids.booleanOr()
- logical OR (empty = false)
Collections:
Monoids.list()
- concatenate lists (empty = [])
Custom:
// Create your own monoid
Monoid<MyType> myMonoid = new Monoid<MyType>() {
public MyType empty() { return MyType.defaultValue(); }
public MyType combine(MyType a, MyType b) { return a.mergeWith(b); }
};
Performance Notes
When to Use HKT vs Direct Methods:
Use HKT When:
- Writing generic code that works with multiple container types
- Building complex workflows with multiple type classes
- You need the power of type class composition
Use Direct Methods When:
- Simple, one-off transformations
- Performance-critical hot paths
- Working with a single, known container type
Examples:
// Hot path - use direct method
Optional<String> result = optional.map(String::toUpperCase);
// Generic reusable code - use HKT
public static <F> Kind<F, String> normalise(Functor<F> functor, Kind<F, String> input) {
return functor.map(String::toUpperCase, input);
}
Memory: HKT simulation adds minimal overhead (single wrapper object per operation) CPU: Direct method calls vs type class method calls are comparable in modern JVMs