Optics: Focus DSL Journey

What You'll Learn

  • Type-safe path navigation with automatic type transitions
  • The FocusPath, AffinePath, and TraversalPath types
  • Effectful modifications with Applicative and Monad
  • Aggregating values with Monoid and foldMap
  • Kind field support and type class integration

Duration: ~35 minutes | Tutorials: 4 | Exercises: 29

Prerequisites: Optics: Lens & Prism Journey

Journey Overview

The Focus DSL provides an ergonomic, type-safe way to navigate nested data structures. Path types automatically widen as you navigate through optional values and collections.

FocusPath → via(Prism) → AffinePath → via(Traversal) → TraversalPath

This is often the most practical way to work with optics in day-to-day code.


Tutorial 12: Focus DSL Basics (~10 minutes)

File: Tutorial12_FocusDSL.java | Exercises: 10

Learn the Focus DSL for ergonomic, type-safe path navigation through nested data structures.

What you'll learn:

  • Creating FocusPath from a Lens with FocusPath.of()
  • Composing paths with via() for deep navigation
  • AffinePath for optional values using some()
  • TraversalPath for collections using each()
  • Accessing specific elements with at(index) and atKey(key)
  • Filtering traversals with filter()
  • Converting paths with toLens(), asAffine(), asTraversal()

Key insight: Path types automatically widen as you navigate. FocusPath becomes AffinePath through optional values, and becomes TraversalPath through collections.

Path type transitions:

FocusPath (Lens-like: exactly 1)
    │
    ├── via(Lens)     → FocusPath
    ├── via(Prism)    → AffinePath
    ├── via(Affine)   → AffinePath
    └── each()        → TraversalPath

AffinePath (0 or 1)
    │
    ├── via(Lens)     → AffinePath
    ├── via(Prism)    → AffinePath
    └── each()        → TraversalPath

TraversalPath (0 to many)
    │
    └── via(anything) → TraversalPath

Example:

// Build a path through nested structure
var path = FocusPath.of(companyLens)       // FocusPath<Root, Company>
    .via(departmentsLens)                   // FocusPath<Root, List<Dept>>
    .each()                                 // TraversalPath<Root, Dept>
    .via(managerLens)                       // TraversalPath<Root, Manager>
    .via(emailLens);                        // TraversalPath<Root, String>

// Get all manager emails
List<String> emails = path.getAll(root);

// Update all manager emails
Root updated = path.modify(String::toLowerCase, root);

Links to documentation: Focus DSL

Hands On Practice


Tutorial 13: Advanced Focus DSL (~10 minutes)

File: Tutorial13_AdvancedFocusDSL.java | Exercises: 8

Master advanced Focus DSL features including type class integration, monoid aggregation, and Kind field navigation.

What you'll learn:

  • modifyF() for effectful modifications with Applicative/Monad
  • foldMap() for aggregating values using Monoid
  • traverseOver() for generic collection traversal via Traverse type class
  • modifyWhen() for conditional modifications
  • instanceOf() for sum type navigation
  • traced() for debugging path navigation

Key insight: traverseOver() bridges the HKT Traverse type class with optics, letting you navigate into Kind<F, A> wrapped collections. This is the foundation for automatic Kind field support in @GenerateFocus.

Effectful modifications:

// Validate while modifying
Either<Error, User> result = path.modifyF(
    EitherMonad.instance(),
    value -> validateAndTransform(value),
    user
);

// Async modification
CompletableFuture<User> futureUser = path.modifyF(
    CFMonad.INSTANCE,
    value -> fetchAndUpdate(value),
    user
);

Aggregation with Monoid:

// Sum all salaries
Integer total = salaryPath.foldMap(
    IntSumMonoid.INSTANCE,
    salary -> salary,
    company
);

// Collect all names
String allNames = namePath.foldMap(
    StringMonoid.INSTANCE,
    name -> name + ", ",
    team
);

Kind field support:

// Manual traverseOver for Kind<ListKind.Witness, Role> field
FocusPath<User, Kind<ListKind.Witness, Role>> rolesKindPath = FocusPath.of(userRolesLens);
TraversalPath<User, Role> allRolesPath = rolesKindPath
    .<ListKind.Witness, Role>traverseOver(ListTraverse.INSTANCE);

// With @GenerateFocus, this is generated automatically:
// TraversalPath<User, Role> roles = UserFocus.roles();

Links to documentation: Kind Field Support | Foldable and Traverse

Hands On Practice


Tutorial 19: Navigator Generation (~10 minutes)

File: Tutorial19_NavigatorGeneration.java | Exercises: 7

Learn how generated navigators enable fluent cross-type navigation, and how SPI-aware path widening determines the correct path type for container fields.

What you'll learn:

  • Navigator delegation: wrapping FocusPath with get/set/modify
  • Path widening through Optional (AffinePath) and List (TraversalPath)
  • SPI-aware widening for Map, Either, Try, and Validated via Cardinality
  • Compound widening rules (AFFINE + TRAVERSAL = TRAVERSAL)
  • Depth limiting with maxNavigatorDepth and fallback to .via()

Key insight: The TraversableGenerator SPI declares a Cardinality for each container type. Navigator generation consults this cardinality to select AffinePath (ZERO_OR_ONE) or TraversalPath (ZERO_OR_MORE), so types like Map, Either, and Try are handled correctly without hardcoding.

Compound widening rules:

FOCUS    + AFFINE    = AFFINE
FOCUS    + TRAVERSAL = TRAVERSAL
AFFINE   + AFFINE    = AFFINE
AFFINE   + TRAVERSAL = TRAVERSAL
TRAVERSAL + anything = TRAVERSAL

Example:

@GenerateFocus(generateNavigators = true)
record Company(String name, Optional<Address> backup) {}

@GenerateFocus(generateNavigators = true)
record Address(String street, Map<String, String> metadata) {}

// Optional (AFFINE) + Map (TRAVERSAL via SPI) = TRAVERSAL
TraversalPath<Company, String> values = CompanyFocus.backup().metadata();

Links to documentation: Focus DSL | Traversal Generator Plugins

Hands On Practice


Tutorial 20: Container Navigation (~5 minutes)

File: Tutorial20_ContainerNavigation.java | Exercises: 4

Navigate container types discovered via the TraversableGenerator SPI, including HKJ native types (Either, Try, Validated) and composition with standard lenses.

What you'll learn:

  • Navigating Either right values via SPI-generated AffinePath
  • Navigating Try success values via SPI-generated AffinePath
  • Composing SPI-aware container paths with lens-based field access
  • Navigating Validated valid values via SPI-generated AffinePath

Key insight: Container navigation paths are generated automatically when @GenerateFocus(generateNavigators = true) is used. The TraversableGenerator SPI determines the cardinality, so Either, Try, and Validated all produce AffinePath navigators without any manual optic composition.

Example:

@GenerateFocus(generateNavigators = true)
record Position(
    String ticker,
    Either<PricingError, MarketPrice> livePrice  // → AffinePath
) {}

// SPI-aware AffinePath navigation
AffinePath<Position, MarketPrice> pricePath = PositionFocus.livePrice();
Optional<MarketPrice> price = pricePath.getOptional(position);

Links to documentation: Focus Containers | Focus Navigation

Hands On Practice


Running the Tutorials

./gradlew :hkj-examples:test --tests "*Tutorial12_FocusDSL*"
./gradlew :hkj-examples:test --tests "*Tutorial13_AdvancedFocusDSL*"
./gradlew :hkj-examples:test --tests "*Tutorial19_NavigatorGeneration*"
./gradlew :hkj-examples:test --tests "*Tutorial20_ContainerNavigation*"

Focus DSL Cheat Sheet

Path Types

TypeFocus CountCreated By
FocusPath<S,A>Exactly 1FocusPath.of(lens)
AffinePath<S,A>0 or 1.via(prism), .some()
TraversalPath<S,A>0 to many.each(), .via(traversal)

Common Operations

OperationAvailable OnDescription
get(s)FocusPathGet the single value
getOptional(s)AffinePathGet optional value
getAll(s)TraversalPathGet all values as List
set(a, s)AllSet value(s)
modify(f, s)AllTransform value(s)
modifyF(m, f, s)AllEffectful modification
foldMap(m, f, s)TraversalPathAggregate with Monoid
MethodEffect
via(lens)Navigate through required field
via(prism)Navigate to sum type variant (widens to Affine)
some()Navigate into Optional (widens to Affine)
each()Navigate into collection (widens to Traversal)
at(index)Navigate to specific index (widens to Affine)
atKey(key)Navigate to map key (widens to Affine)
filter(pred)Filter traversal targets

Common Pitfalls

1. Expecting get() on AffinePath

Problem: Calling get() on an AffinePath when you need getOptional().

Solution: AffinePath might have zero elements. Use getOptional():

Optional<String> value = affinePath.getOptional(source);

2. Type Inference Issues with modifyF

Problem: Java can't infer type parameters for modifyF.

Solution: Explicitly specify the monad instance:

path.<EitherKind.Witness<Error>>modifyF(EitherMonad.instance(), ...)

3. Forgetting traverseOver for Kind Fields

Problem: Can't navigate into Kind<ListKind.Witness, A> field.

Solution: Use traverseOver with the appropriate Traverse instance:

path.traverseOver(ListTraverse.INSTANCE)

What's Next?

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

  • Lens, Prism, Affine, and Traversal
  • Optic composition rules
  • Generated optics with annotations
  • The Fluent API and Free Monad DSL
  • The Focus DSL for type-safe navigation

Recommended next steps:

  1. Effect API Journey: Combine optics with Effect paths
  2. Use @GenerateFocus: Annotate your records for automatic path generation
  3. Study Production Examples: See Draughts Game
  4. Explore Core Types: Understand the HKT foundation powering modifyF

Previous: Optics: Fluent & Free DSL Next: Expression: ForState