ForPath Examples

This chapter provides worked examples of ForPath comprehensions for each major Path type, showing how different effect semantics shape the comprehension behaviour.

EitherPath: Railway-Oriented Error Handling

Real-world workflows often chain operations where any step can fail. Writing explicit error checks after every call obscures the happy path. EitherPath comprehensions give you railway-oriented programming: the happy path reads top-to-bottom, and any failure automatically short-circuits to a Left without additional boilerplate.

record User(String id, String name) {}
record Order(String orderId, User user) {}

Function<String, EitherPath<String, User>> findUser = id ->
    id.equals("user-1")
        ? Path.right(new User("user-1", "Alice"))
        : Path.left("User not found: " + id);

Function<User, EitherPath<String, Order>> createOrder = user ->
    Path.right(new Order("order-123", user));

EitherPath<String, String> result = ForPath.from(findUser.apply("user-1"))
    .from(user -> createOrder.apply(user))
    .yield((user, order) -> "Created " + order.orderId() + " for " + user.name());
// Right("Created order-123 for Alice")

EitherPath<String, String> failed = ForPath.from(findUser.apply("unknown"))
    .from(user -> createOrder.apply(user))
    .yield((user, order) -> "Created " + order.orderId());
// Left("User not found: unknown")

The second comprehension never reaches createOrder -- the Left from findUser propagates immediately.


IOPath: Deferred Side Effects

Not every computation should run immediately. IOPath wraps side-effectful operations (reading files, calling APIs, writing logs) as descriptions of work rather than executing them eagerly. The comprehension builds a pipeline that only runs when you explicitly ask for the result, giving you full control over when effects happen.

IOPath<String> readConfig = Path.io(() -> "production");
IOPath<Integer> readPort = Path.io(() -> 8080);

IOPath<String> serverInfo = ForPath.from(readConfig)
    .from(env -> readPort)
    .let(t -> t._1().toUpperCase())
    .yield((env, port, upperEnv) -> upperEnv + " server on port " + port);

// Nothing executes until:
String result = serverInfo.unsafeRun();  // "PRODUCTION server on port 8080"

This separation of description from execution makes IO pipelines easy to test, compose, and reason about.


VTaskPath: Virtual Thread Concurrency

When your workflow involves I/O-bound service calls, you want those calls running on virtual threads so the JVM can handle thousands of them concurrently. VTaskPath wraps each step as a virtual-thread task, and the comprehension orchestrates them sequentially (each step can depend on the previous result) or in parallel with par().

VTaskPath<User> fetchUser = Path.vtask(() -> userService.fetch(userId));
VTaskPath<Profile> fetchProfile = Path.vtask(() -> profileService.fetch(profileId));

VTaskPath<String> greeting = ForPath.from(fetchUser)
    .from(user -> fetchProfile)
    .let(t -> t._1().name().toUpperCase())
    .yield((user, profile, upperName) ->
        "Hello " + upperName + " from " + profile.city());

// Nothing executes until:
String result = greeting.unsafeRun();  // "Hello ALICE from London"

VTaskPath comprehensions are particularly well-suited for orchestrating multi-step service workflows where each step depends on the previous:

VTaskPath<OrderSummary> orderWorkflow = ForPath.from(Path.vtask(() -> validateOrder(order)))
    .from(validated -> Path.vtask(() -> reserveInventory(validated)))
    .from(t -> Path.vtask(() -> processPayment(t._2())))
    .from(t -> Path.vtask(() -> sendConfirmation(t._3())))
    .yield((validated, reserved, payment, confirmation) ->
        new OrderSummary(validated.id(), payment.transactionId(), confirmation.sentAt()));

// Execute the entire workflow
Try<OrderSummary> result = orderWorkflow.runSafe();

NonDetPath: Nondeterministic Choice

The name NonDet comes from nondeterministic computation -- a model where a computation can produce multiple results simultaneously, as if it were exploring every possible choice in parallel. This concept originates from nondeterministic automata in theoretical computer science, where a machine can be "in multiple states at once."

In practice, NonDetPath is backed by List, and a for-comprehension over it generates the cartesian product of all choices. Combined with when() guards, this makes it a natural fit for search problems, constraint satisfaction, and combinatorial generation.

NonDetPath<String> combinations = ForPath.from(Path.list("red", "blue"))
    .from(c -> Path.list("S", "M", "L"))
    .when(t -> !t._1().equals("blue") || !t._2().equals("S"))  // filter out blue-S
    .yield((colour, size) -> colour + "-" + size);

List<String> result = combinations.run();
// ["red-S", "red-M", "red-L", "blue-M", "blue-L"]

Each .from() introduces a new "dimension" of choice, and when() prunes branches that don't satisfy constraints -- much like a SELECT ... WHERE over multiple tables.


Extended Arity (6+ Bindings)

All ForPath types support up to 12 chained bindings. Step 1 is hand-written; steps 2-12 are generated by the hkj-processor annotation processor:

// A 6-step MaybePath comprehension
MaybePath<String> profile = ForPath.from(Path.just("user-42"))
    .from(id -> findUser(id))                    // b = User
    .focus(user -> user.address())               // c = Address
    .focus(addr -> addr.city())                  // d = city name
    .let(t -> t._4().toUpperCase())              // e = uppercased city
    .let(t -> t._2().name() + " from " + t._5()) // f = summary
    .yield((id, user, addr, city, upper, summary) -> summary);

// Result: Just("Alice from LONDON")

At higher arities, the tuple-style yield can be more readable:

.yield(t -> t._6())  // access the summary directly by position

Previous: ForPath Comprehension | Next: ForPath Parallel Composition