Optics: Focus DSL Journey
- 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
FocusPathfrom a Lens withFocusPath.of() - Composing paths with
via()for deep navigation AffinePathfor optional values usingsome()TraversalPathfor collections usingeach()- Accessing specific elements with
at(index)andatKey(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
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/MonadfoldMap()for aggregating values using MonoidtraverseOver()for generic collection traversal via Traverse type classmodifyWhen()for conditional modificationsinstanceOf()for sum type navigationtraced()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
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
maxNavigatorDepthand 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
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
Eitherright values via SPI-generatedAffinePath - Navigating
Trysuccess values via SPI-generatedAffinePath - Composing SPI-aware container paths with lens-based field access
- Navigating
Validatedvalid values via SPI-generatedAffinePath
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
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
| Type | Focus Count | Created By |
|---|---|---|
FocusPath<S,A> | Exactly 1 | FocusPath.of(lens) |
AffinePath<S,A> | 0 or 1 | .via(prism), .some() |
TraversalPath<S,A> | 0 to many | .each(), .via(traversal) |
Common Operations
| Operation | Available On | Description |
|---|---|---|
get(s) | FocusPath | Get the single value |
getOptional(s) | AffinePath | Get optional value |
getAll(s) | TraversalPath | Get all values as List |
set(a, s) | All | Set value(s) |
modify(f, s) | All | Transform value(s) |
modifyF(m, f, s) | All | Effectful modification |
foldMap(m, f, s) | TraversalPath | Aggregate with Monoid |
Navigation
| Method | Effect |
|---|---|
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:
- Effect API Journey: Combine optics with Effect paths
- Use @GenerateFocus: Annotate your records for automatic path generation
- Study Production Examples: See Draughts Game
- Explore Core Types: Understand the HKT foundation powering
modifyF
Previous: Optics: Fluent & Free DSL Next: Expression: ForState