Core Types: Advanced Patterns Journey

What We'll Learn

  • Natural Transformations for polymorphic type conversion
  • Coyoneda for free functors and map fusion optimisation
  • Free Applicative for modelling independent, parallelisable computations
  • Static analysis of programs before execution

Duration: ~40 minutes | Tutorials: 4 | Exercises: 26

Prerequisites: Core Types: Error Handling Journey

Where This Fits in the Bigger Picture

The .toEitherPath() token in One Line, Six Layers is a natural transformation; this journey is the hands-on counterpart to that idea. It pairs with Natural Transformation, Coyoneda, Free Applicative, and the Foundations FAQ entries on parallel-vs-sequential composition.

Journey Overview

This journey covers advanced functional programming patterns. These aren't everyday tools, but when you need them, they're invaluable. You'll learn how to transform between type constructors, optimise mapping operations, and model parallel computations.

Natural Transformations → Coyoneda (map fusion) → Free Applicative (parallel)

Tutorial 08: Natural Transformations (~10 minutes)

File: Tutorial08_NaturalTransformation.java | Exercises: 5

Learn to transform between type constructors while preserving structure.

What you'll learn:

  • What natural transformations are and why they matter
  • The Natural<F, G> interface for polymorphic transformations
  • Composing transformations with andThen and compose
  • Using transformations to interpret Free structures
  • The identity transformation and its uses

Key insight: Natural transformations let you change the "container" without touching the contents. They're the morphisms between functors.

Real-world application: Interpreting DSLs, converting between effect types, abstracting over different backends.

Links to documentation: Natural Transformation Guide

Hands On Practice


Tutorial 09: Coyoneda (~10 minutes)

File: Tutorial09_Coyoneda.java | Exercises: 5

Learn how Coyoneda gives you a free Functor and enables map fusion.

What you'll learn:

  • Lifting values into Coyoneda with lift
  • Mapping without a Functor instance (deferred execution)
  • Map fusion: chaining maps accumulates into one traversal
  • Lowering back to execute accumulated transformations
  • When and why to use Coyoneda

Key insight: Coyoneda stores the value and accumulated function separately, executing only when you lower. Multiple maps become a single traversal.

Example:

// Without Coyoneda: 3 separate traversals
list.map(f).map(g).map(h);

// With Coyoneda: functions compose, single traversal on lower
Coyoneda.lift(list)
    .map(f)
    .map(g)
    .map(h)
    .lower(listFunctor);  // One traversal with f.andThen(g).andThen(h)

Real-world application: Optimising repeated transformations, working with types that lack Functor instances, building efficient pipelines.

Links to documentation: Coyoneda Guide

Hands On Practice


Tutorial 10: Free Applicative (~10 minutes)

File: Tutorial10_FreeApplicative.java | Exercises: 6

Learn to model independent computations that can potentially run in parallel.

What you'll learn:

  • Creating pure values with FreeAp.pure
  • Lifting instructions with FreeAp.lift
  • Combining independent computations with map2
  • Interpreting programs with foldMap
  • The key difference from Free Monad: independence vs dependence

Key insight: With Free Applicative, neither computation in map2 depends on the other's result. This structural independence enables parallel execution.

Comparison:

// Free Monad (sequential - B depends on A)
freeA.flatMap(a -> computeB(a))

// Free Applicative (parallel - independent)
FreeAp.map2(freeA, freeB, (a, b) -> combine(a, b))

Real-world application: Parallel data fetching, form validation, batch API calls, request deduplication.

Links to documentation: Free Applicative Guide

Hands On Practice


Tutorial 11: Static Analysis (~10 minutes)

File: Tutorial11_StaticAnalysis.java | Exercises: 10

Learn to analyse Free Applicative programs before execution.

What you'll learn:

  • Counting operations with FreeApAnalyzer.countOperations
  • Collecting all operations with FreeApAnalyzer.collectOperations
  • Checking for dangerous operations before execution
  • Grouping operations by type for batching
  • Custom analysis with Const functor and Monoid
  • Under/Over semantics with SelectiveAnalyzer
  • Effect bounds and partitioning

Key insight: Because Free Applicative captures program structure as data, you can inspect it before running. This enables permission checking, cost estimation, and optimisation.

Example:

// Build a program
FreeAp<DbOp, Dashboard> program = buildDashboard(userId);

// Analyse before running
int opCount = FreeApAnalyzer.countOperations(program);
boolean hasDeletions = FreeApAnalyzer.containsOperation(
    program,
    op -> DbOp.Delete.class.isInstance(DbOpHelper.DB_OP.narrow(op))
);

if (hasDeletions && !userHasPermission("delete")) {
    throw new SecurityException("Delete operations not permitted");
}

Real-world application: Permission checking, audit logging, cost estimation, query batching, security validation.

Links to documentation: Choosing Abstraction Levels

Hands On Practice


Running the Tutorials

./gradlew :hkj-examples:test --tests "*Tutorial08_NaturalTransformation*"
./gradlew :hkj-examples:test --tests "*Tutorial09_Coyoneda*"
./gradlew :hkj-examples:test --tests "*Tutorial10_FreeApplicative*"
./gradlew :hkj-examples:test --tests "*Tutorial11_StaticAnalysis*"

Key Concepts Summary

Natural Transformation

A polymorphic function between type constructors: Natural<F, G> transforms Kind<F, A> to Kind<G, A> for any type A. Used to interpret Free structures.

Coyoneda

The "free functor" that gives any type a Functor instance. Stores a value and an accumulated function, enabling map fusion where multiple maps become a single traversal.

Free Applicative

Captures independent computations that can potentially run in parallel. Unlike Free Monad's flatMap, map2 combines values without creating dependencies.

Static Analysis

FreeApAnalyzer and SelectiveAnalyzer provide utilities for inspecting Free Applicative programs before execution. Count operations, check for dangerous effects, group operations for batching, and compute effect bounds.


Common Pitfalls

1. Using Free Monad When Free Applicative Suffices

Problem: Using flatMap everywhere, missing parallelisation opportunities.

Solution: Ask: "Does this step need the previous result?" If no, use map2.

2. Forgetting to Lower Coyoneda

Problem: Building up Coyoneda but never executing with lower().

Solution: Coyoneda is lazy. Call lower(functor) to execute.

3. Type Parameter Confusion with Natural

Problem: Getting lost in Natural<F, G> type parameters.

Solution: Think of it as: "I can turn any F<A> into a G<A>".


What's Next?

Congratulations! You've completed the Core Types track. You now understand:

  • The HKT simulation in Java
  • Functor, Applicative, and Monad
  • Error handling with MonadError
  • Advanced patterns for optimisation and parallelism

Recommended next steps:

  1. Effect API Journey: Learn the user-friendly path-based API (recommended)
  2. Optics: Lens & Prism: Apply functional patterns to data manipulation
  3. Monad Transformers: Stack effects with EitherT, ReaderT, etc.
  4. Study Real Examples: See Order Workflow

Previous: Core Types: Error Handling Next: Effect API