{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreieb33iqpruakbuachf6fojvkso2hvl6iw2554civaiist6pitzsuy",
"uri": "at://did:plc:vdlfckiwtrxaiw5e6ky6vz4z/app.bsky.feed.post/3meoumhi763w2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreicbjibhct4g6gkxjpc4iroglrxk5pnf4b6gaj2hn3yqrjalypckxq"
},
"mimeType": "image/jpeg",
"size": 76234
},
"path": "/2026/02/12/mfj-from-theory-to-practice.html",
"publishedAt": "2026-02-12T00:00:00.000Z",
"site": "https://blog.scottlogic.com",
"tags": [
"`Pipeline`",
"`PipelineError`",
"`ParallelPipeline`",
"VTask documentation",
"Run the demo yourself",
"Error Handling Journey",
"`hkj-spring-boot-starter`",
"Zero-config",
"Auto status mapping",
"Error accumulation",
"Async support",
"Actuator metrics",
"Security integration",
"JSON serialisation",
"Try it now",
"Migration Guide",
"spec interfaces",
"Optics for External Types",
"Focus DSL Tutorial",
"Error Handling Journey",
"VTask Journey",
"Focus DSL documentation",
"Value Types",
"Carrier Classes",
"Lazy Constants",
"Focus DSL Guide",
"Effect Path API",
"VTask and Scope",
"Optics for External Types",
"Spring Boot Integration",
"Migration Guide",
"Tutorials home",
"Data-Oriented Programming in Java",
"Simple Made Easy",
"lens library",
"_Optics by Example_",
"@GetMapping",
"@PathVariable",
"@ExceptionHandler",
"@ExceptionHandler",
"@GetMapping",
"@PathVariable",
"@PostMapping",
"@RequestBody",
"@GenerateLenses",
"@ImportOptics",
"@ImportOptics",
"@GenerateLenses",
"@GenerateLenses",
"@ImportOptics",
"@GenerateFocus",
"@GenerateFocus",
"@GenerateFocus",
"@GenerateLenses",
"@ImportOptics",
"@ImportOptics"
],
"textContent": "# From Theory to Practice\n\n_Part 6 of the Functional Optics for Modern Java series_\n\nWe set out noting a frustration that Java handles _reading_ nested structures elegantly, but _writing_ them remains painful. Over five articles, we built a response: optics for navigation, effects for error handling, and a bridge between them.\n\nNow it’s time to see everything working together and to be honest about when to use these patterns and when simpler approaches suffice.\n\n* * *\n\n## The Complete Toolkit\n\nLike surgical instruments, the **Focus DSL** provides precision tools for navigating to exactly the right location and making targeted modifications. The **Effect Path API** provides the monitors: tracking what can go wrong, accumulating diagnostics, coordinating concurrent operations. Neither replaces the other. Together, they enable surgical precision on complex data structures.\n\nFor details on Focus DSL, see Part 4. For Effect Paths, see Part 5.\n\n### The Three-Layer Architecture\n\nHigher-Kinded-J is built in layers, each serving a different audience:\n\n * **Layer 1** provides the mathematical foundation: higher-kinded type simulation via the Witness pattern. This is what allows generic code to work across `List`, `Option`, `Either`, and `Future`.\n\n * **Layer 2** provides the standard monad transformers (`EitherT`, `StateT`, `ReaderT`). In Scala libraries like Cats, these are the primary user-facing types. But in Java, `EitherT<CompletableFutureKind, Error, User>` is syntactically intimidating.\n\n * **Layer 3** is where most code lives. The Effect Path API wraps transformers into fluent, concrete classes. When you call `Path.either(value)`, the library internally constructs the appropriate transformer stack. You never see `Kind<F, A>` unless you want to.\n\n\n\n\nThis layering acknowledges a key insight: **Java developers prefer fluent interfaces over type class constraints**. The Effect Path API is essentially a Domain-Specific Language (DSL) for monad transformers, designed to feel like Java’s Stream API rather than Haskell’s do-notation.\n\nThe Focus DSL provides the same treatment for optics: fluent navigation without explicit optic composition. And the bridge between them (`path.focus(lens).modify(fn)`) enables surgical precision for data even when wrapped in effects.\n\n* * *\n\n## The Pipeline in Action\n\nWith all our pieces in place, we have a complete expression language implementation. The `Pipeline` class composes four phases, each using the appropriate effect type:\n\nThe key is how effects are explicit in the types:\n\n\n public Either<PipelineError, Object> run(String source, Environment env) { return parser.apply(source) .mapLeft(PipelineError::fromParseError) .flatMap(ast -> typeChecker.apply(ast).fold( errors -> Either.left(PipelineError.fromTypeErrors(errors)), type -> { Expr optimised = optimiser.apply(ast); Object result = interpreter.apply(optimised).apply(env); return Either.right(result); } )); }\n\nNotice how `PipelineError` is a sealed interface with variants for each failure mode. Pattern matching on the result gives exhaustive error handling.\n\n### Parallel Pipeline\n\nFor concurrent operations, `ParallelPipeline` demonstrates VTask with Scope:\n\n\n List<Validated<List<TypeError>, Type>> results = parallelPipeline.typeCheckAllParallel(expressions, typeEnv);\n\nThe `Scope` API provides structured concurrency patterns:\n\nJoiner | Behaviour | Use Case\n---|---|---\n`allSucceed` | Wait for all, fail-fast on error | Batch operations that must all complete\n`anySucceed` | First success wins, cancel others | Redundant calls, fallbacks\n`accumulating` | Collect all results OR all errors | Validation, comprehensive reporting\n\nSee VTask documentation for details.\n\nRun the demo yourself:\n\n\n ./gradlew :run -PmainClass=org.higherkindedj.article6.demo.Article6Demo\n\n* * *\n\n## Traditional Java vs Higher-Kinded-J\n\nThe patterns we’ve developed solve real problems. Here’s how they compare to traditional approaches:\n\nChallenge | Traditional Java | Higher-Kinded-J | Details\n---|---|---|---\nDeep updates | Copy-constructor cascade (25+ lines) | Lens composition (1 line) | Part 1\nNull handling | `if (x != null)` chains | `MaybePath` makes absence explicit | Part 5\nError handling | Nested try-catch pyramids | `EitherPath` railway model | Part 5\nValidation | First-error-only | `ValidationPath` accumulates ALL errors | Part 5\nConcurrency | `CompletableFuture` callbacks | `VTaskPath` + `Scope` | Part 5\n\nThe shape of the transformation:\n\n\n // Traditional: nested, inside-out, implicit errors if (user != null) { if (validator.validate(request).isValid()) { try { return paymentService.charge(user, request); } catch (PaymentException e) { // handle... } } } // Higher-Kinded-J: flat, top-to-bottom, explicit errors Path.maybe(findUser(userId)) .toEitherPath(() -> new UserNotFound(userId)) .via(user -> validate(request)) .via(req -> Path.tryOf(() -> paymentService.charge(user, req)))\n\nFor comprehensive patterns, see the Error Handling Journey tutorial.\n\n* * *\n\n## For Spring Developers\n\nIf you use Spring Boot, the `hkj-spring-boot-starter` brings these patterns directly into your controllers. The integration is non-invasive: existing exception-based endpoints continue to work, and you can adopt functional error handling incrementally.\n\n### The Transformation\n\nException-based error handling hides failure modes in implementation details. Functional error handling makes them explicit:\n\n\n // Before: What can fail? Read the implementation to find out. @GetMapping(\"/{id}\") public User getUser(@PathVariable String id) { return userService.findById(id); } @ExceptionHandler(UserNotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException ex) { return ResponseEntity.status(404).body(new ErrorResponse(ex.getMessage())); } // After: Errors are explicit in the signature. No @ExceptionHandler needed. @GetMapping(\"/{id}\") public Either<DomainError, User> getUser(@PathVariable String id) { return userService.findById(id); // Right(user) → HTTP 200 // Left(UserNotFoundError) → HTTP 404 (by naming convention) }\n\n### What the Starter Provides\n\nCapability | What It Does\n---|---\nZero-config | Add dependency, return `Either`/`Validated` from controllers\nAuto status mapping | Error class names map to HTTP status codes\nError accumulation | `Validated<List<Error>, User>` returns ALL validation errors\nAsync support | `CompletableFuturePath` for non-blocking operations\nActuator metrics | Track success/error rates, latency percentiles\nSecurity integration | `ValidatedUserDetailsService` for functional authentication\nJSON serialisation | Configurable formats (TAGGED, UNWRAPPED, DIRECT)\n\n### Validation That Reports ALL Errors\n\nTraditional validation stops at the first error. Users fix one problem, submit again, discover another. With `Validated`, they see everything at once:\n\n\n @PostMapping public Validated<List<ValidationError>, User> createUser(@RequestBody UserRequest request) { return userService.validateAndCreate(request); // Valid(user) → HTTP 200 with user JSON // Invalid(errors) → HTTP 400 with ALL validation errors: // [{\"field\": \"email\", \"message\": \"Invalid format\"}, // {\"field\": \"age\", \"message\": \"Must be positive\"}, // {\"field\": \"name\", \"message\": \"Required\"}] }\n\nThe framework accumulates errors automatically. No more “whack-a-mole” validation for your users.\n\n### Production Monitoring\n\nThe Actuator integration tracks functional operations in production:\n\n\n curl http://localhost:8080/actuator/hkj\n\n\n { \"metrics\": { \"eitherPath\": { \"successCount\": 1547, \"errorCount\": 123, \"successRate\": 0.926 }, \"validationPath\": { \"validCount\": 892, \"invalidCount\": 45, \"validRate\": 0.952 } } }\n\n**Try it now:**\n\n\n ./gradlew :hkj-spring:example:bootRun\n\nSee the complete Migration Guide for step-by-step patterns: converting exceptions to Either, validation to Validated, and async operations to EitherT.\n\n* * *\n\n## Working with Third-Party Types\n\nYour domain model uses `@GenerateLenses`, but what about JDK classes like `LocalDate` or library types like Jackson’s `JsonNode`? You can’t annotate code you don’t own.\n\n**`@ImportOptics` solves this.** Add it to a `package-info.java`:\n\n\n @ImportOptics({ java.time.LocalDate.class, java.time.LocalTime.class }) package com.myapp.optics; import org.higherkindedj.optics.annotations.ImportOptics;\n\nThe processor analyses each type and generates appropriate optics:\n\nNow you can compose across the boundary between your types and external types:\n\n\n // Your record @GenerateLenses record Order(String id, Customer customer, LocalDate orderDate) {} // Composition: orderDate lens → year lens var nextYearOrder = OrderLenses.orderDate() .andThen(LocalDateLenses.year()) .modify(y -> y + 1, order);\n\n### Supported External Types\n\nPattern | Example Types | Generated Optics\n---|---|---\nRecords | Third-party records | Lenses via constructor\nSealed interfaces | Sum types | Prisms for each variant\nEnums | Status codes | Prisms for each constant\nWither classes | `LocalDate`, `LocalTime` | Lenses via `withX()` methods\nBuilders | JOOQ, Protobuf | Via spec interfaces\n\nFor complex cases like Jackson’s `JsonNode` (which uses predicates rather than sealed types), see the Optics for External Types guide.\n\n* * *\n\n## Migration Path\n\nAdoption can be incremental. Start small, prove the pattern, then expand:\n\n 1. **Annotate one type** - Add `@GenerateLenses` to a domain record\n 2. **Replace one cascade** - Convert a deep update to lens composition\n 3. **Wrap exceptions** - Use `TryPath` for one exception-throwing call\n 4. **Accumulate validation** - Switch one validator from first-error to `ValidationPath`\n 5. **Import external types** - Add `@ImportOptics` for JDK types you frequently modify\n 6. **Enable navigators** - Add `@GenerateFocus(generateNavigators = true)` for fluent navigation\n\n\n\nEach step is independent. You don’t need to convert everything at once.\n\nFor complete guidance, see:\n\n * Focus DSL Tutorial\n * Error Handling Journey\n * VTask Journey\n\n\n\n* * *\n\n## Design Patterns\n\nSeveral patterns crystallised through the series:\n\nPattern | Description | When to Use\n---|---|---\n**Focus-Path-First** | Annotate types with `@GenerateFocus`, express operations as paths | Any nested domain model\n**Effect Stratification** | Different phases use different effects (Either for parsing, Validated for checking) | Pipelines with distinct stages\n**Paths as Configuration** | Store paths as values, compose at runtime | Configurable queries, reporting\n**Validated for Users** | Accumulate all errors for user-facing validation | Forms, API requests\n\n### Focus-Path-First in Practice\n\nThe benefit of Focus-Path-First design is that adding new operations requires zero changes to your types. Define paths once, reuse everywhere:\n\n\n @GenerateFocus(generateNavigators = true) record Department(String name, List<Employee> employees, Employee manager) {} // Define paths once TraversalPath<Department, Employee> allEmployees = DepartmentFocus.employees().each(); // Use for any operation: queries, updates, validations List<String> names = allEmployees.getAll(dept).stream() .map(Employee::name).toList(); Department withRaises = allEmployees.modifyAll( emp -> emp.withSalary(emp.salary().multiply(1.1)), dept); ValidationPath<List<Error>, Department> validated = allEmployees.toValidationPath(emp -> validateEmployee(emp), dept);\n\n### Effect Stratification\n\nDifferent phases of processing need different effects. Making this explicit in types communicates intent:\n\nWhen you see `Validated` in a signature, you know errors will accumulate. When you see `State`, you know context is being threaded. The types communicate intent.\n\nFor detailed examples of each pattern, see the Focus DSL documentation.\n\n* * *\n\n## When to Keep It Simple\n\nHonesty requires acknowledging when these abstractions aren’t worth it:\n\n**Use Focus DSL when:** - Structures are three or more levels deep - The same path is accessed in multiple places - Code is read more often than executed\n\n**Keep it simple when:** - Structures are shallow (one or two levels) - Transformations are one-off - Team is novice (learning curve is real) - Performance-critical inner loops (measure first)\n\nRich Hickey’s distinction applies: optics are _simple_ (few concepts, composable) but not always _easy_ (there’s a learning curve). Know when the abstraction pays for itself.\n\nThat said, **the learning curve argument is weakening**. Five years ago, functional patterns in Java felt like fighting the language. Today, with records, sealed interfaces, pattern matching, and virtual threads, Java actively supports these idioms. The fact that Higher-Kinded-J is not just _possible_ but _practical_ in modern Java shows how far the language has come—and how well these patterns align with Java’s direction.\n\n* * *\n\n## Where We Stand\n\nHigher-Kinded-J joins a family of optics libraries across languages. Haskell’s lens, Scala’s Monocle, and Kotlin’s Arrow all provide mature, well-tested implementations of optics. Each is idiomatic to its language.\n\nWhat Higher-Kinded-J brings is **optics designed for Java from the ground up** —not a port of Haskell idioms, but an implementation that embraces records, sealed interfaces, annotation processing, and virtual threads as first-class features.\n\n### Java First, Not an Imitation\n\nMany functional libraries in Java are ports of Haskell or Scala libraries, bringing foreign idioms that feel awkward. Higher-Kinded-J takes a different approach: **Java first**.\n\nWe adopt good ideas from other languages, but Higher-Kinded-J is designed for modern Java:\n\n * **Records and sealed interfaces** are first-class citizens, not afterthoughts\n * **Pattern matching** complements our optics rather than competing with them\n * **Annotation processing** generates idiomatic Java, not Haskell-in-Java\n * **The Focus DSL** uses Java’s method chaining naturally\n * **Virtual threads** provide concurrency without callback complexity\n\n\n\nThe goal isn’t to make Java feel like Haskell. It’s to give Java developers powerful abstractions while respecting Java’s idioms. When you use Higher-Kinded-J, you’re writing modern, expressive, functional Java.\n\n### Aligned with Java’s Future\n\nHigher-Kinded-J already embraces structured concurrency through VTask and Scope. But Java’s roadmap suggests the alignment will only deepen:\n\n**Value Types** (Project Valhalla) will let classes opt out of identity semantics. Optic wrappers like `FocusPath` could become value types—zero allocation overhead, compared by structure rather than identity. The performance gap between direct field access and optic-based access could effectively disappear.\n\n**Carrier Classes** extend the record model with derived state, mutable fields, and inheritance—while preserving pattern matching and `with` expressions. Since `@GenerateLenses` already works with records, extending support to carrier classes is natural. The `with` expressions in carrier classes align perfectly with lens-based modification.\n\n**Lazy Constants** (JEP 526, previewing in JDK 26) provide thread-safe deferred initialization with JIT-level constant folding. Combined with optics, this could mean paths that compose eagerly but execute lazily—defining a deep traversal costs nothing until you actually use it, and once resolved, the JIT can treat it as a true constant.\n\nThe pattern is clear: Java is evolving toward richer data-oriented features, and Higher-Kinded-J is positioned to take advantage of each advance. Learning optics and effects today is an investment that becomes more valuable as Java matures.\n\n* * *\n\n## Completing the Picture\n\nJava 25 gives us records, sealed interfaces, and pattern matching for _reading_ data elegantly. Higher-Kinded-J completes the picture:\n\n * **Focus DSL** provides the _write_ side that pattern matching lacks\n * **Effect Path API** provides composable error handling\n * **@ImportOptics** extends optics to types you don’t own\n\n\n\n\n // Pattern matching reads if (company instanceof Company(_, var departments)) { ... } // Focus DSL writes Company updated = CompanyFocus.departments().each() .employees().each() .salary() .modifyAll(s -> s.multiply(1.1), company); // Effect Path API validates ValidationPath<List<Error>, Company> validated = Path.valid(company, Semigroups.list()) .via(c -> validateAllEmployees(c));\n\nThe two APIs work in harmony. Together, they provide a complete functional programming toolkit for Java.\n\n### The Composability Principle\n\nA theme running through this series is composition. Focus paths compose with `via()`. Collection navigation composes with `each()`. Effects compose via type classes. Each composition multiplies capability without multiplying complexity.\n\nThis is the result of principled abstraction. When your building blocks follow laws (lens laws, functor laws, applicative laws), composition just works. You don’t verify each combination manually; the laws guarantee sensible behaviour.\n\nEric Normand captures this in his work on data-oriented programming: build with small pieces that combine predictably. Rich Hickey emphasises simplicity over ease: simple things compose, easy things often don’t. The Focus DSL and Effect Path API embody these principles in Java.\n\n* * *\n\n## Further Reading\n\n### This Series\n\n * Part 1: The Immutability Gap - The problem we set out to solve\n * Part 2: Optics Fundamentals - Lenses, prisms, traversals\n * Part 3: AST with Basic Optics - Applying optics to a real domain\n * Part 4: Traversals and Pattern Rewrites - The Focus DSL\n * Part 5: The Effect Path API - Railway-style error handling\n\n\n\n### Higher-Kinded-J Documentation\n\n * **Focus DSL Guide** - Complete Focus DSL reference\n * **Effect Path API** - Effect types and patterns\n * **VTask and Scope** - Virtual thread concurrency\n * **Optics for External Types** - `@ImportOptics` for JDK and library types\n * **Spring Boot Integration** - Using HKJ with Spring\n * **Migration Guide** - From exceptions to functional errors\n * **Tutorials home** - Learn Higher-Kinded-J following tutorials\n\n\n\n### Background\n\n * **Brian Goetz,Data-Oriented Programming in Java** - DOP principles for Java\n * **Rich Hickey,Simple Made Easy** - Simple vs easy\n * **Edward Kmett’slens library** - The Haskell gold standard\n * **Chris Penner,_Optics by Example_** - A major influence on Higher-Kinded-J; the best practical guide to optics\n\n\n\n> “Optics are a family of inter-composable combinators for building bidirectional data transformations.” — Chris Penner, _Optics by Example_\n\n* * *\n\n_This concludes the Functional Optics for Modern Java series. Thank you for reading._",
"title": "Functional Optics for Modern Java - Part 6",
"updatedAt": "2026-02-12T00:00:00.000Z"
}