Glossary: Effect Paths & Effect Handlers

What This Page Covers

  • The Effect Path API and the Free-monad effect-handler machinery.
  • Part of the Glossary; see it for the other categories.

BoundSet

Definition: A generated container holding Bound instances for each effect algebra in a composition. Each Bound provides smart constructors that automatically inject operations into the correct position in the composed EitherF chain. Obtained from the generated *Wiring.boundSet() method.

Example:

var bounds = AppEffectsWiring.boundSet();
var console = bounds.console();  // Bound<ComposedType> for ConsoleOp
var db = bounds.db();            // Bound<ComposedType> for DbOp

Free<ComposedType, String> program =
    console.readLine(Function.identity())
        .flatMap(name -> db.save(name, Function.identity()));

Related: @ComposeEffects, Inject


@ComposeEffects

Definition: An annotation processor that generates composition infrastructure for multiple effect algebras: Inject instances, a composed Functor, and a BoundSet for program construction. Annotate a record whose fields are Class<?> references to the effect algebras being composed.

Example:

@ComposeEffects
public record AppEffects(
    Class<ConsoleOp<?>> console,
    Class<DbOp<?>> db) {}
// Generates: AppEffectsWiring with boundSet(), interpret(), etc.

Related: EitherF, Inject, BoundSet


Continuation-Passing Style (CPS)

Definition: A pattern where each operation record includes a Function parameter (conventionally named k) that transforms the operation's natural result type to the generic type parameter A. If a Charge operation naturally produces a ChargeResult, the continuation Function<ChargeResult, A> lets callers transform that result inline. This is the same idea as CompletableFuture.thenApply: chain a transformation onto a value that does not exist yet.

Example:

// The continuation k transforms ChargeResult → A
record Charge<A>(Money amount, PaymentMethod method,
    Function<ChargeResult, A> k) implements PaymentGatewayOp<A> { }

// Using Function.identity() returns the natural result type directly
Free<G, ChargeResult> charge =
    gateway.charge(amount, method, Function.identity());

// Using a custom continuation transforms the result inline
Free<G, TransactionId> txId =
    gateway.charge(amount, method, result -> result.transactionId());

Why CPS: Enables proper Java type inference at call sites. Without the continuation, the compiler cannot infer the relationship between the operation's result and the program's type parameter.

Related: Effect Algebra, mapK


Effect

Definition: A computational context that represents a value alongside some additional behaviour or outcome. Effects model computations that may fail, produce optional results, perform side effects, or require asynchronous execution. Rather than throwing exceptions or returning null, effects make these behaviours explicit in the type system.

Common Effect Types in Higher-Kinded-J:

  • Maybe - Computation that may produce no result
  • Either - Computation that may fail with typed error information
  • Try - Computation that may throw an exception
  • IO - Computation that performs side effects
  • Validated - Computation that accumulates multiple errors

Example:

// Without effects: hidden failure modes
User getUser(String id) {
    // Might return null? Throw exception? Which exceptions?
}

// With effects: explicit about what can happen
Either<UserError, User> getUser(String id) {
    // Returns Right(user) on success, Left(error) on failure
}

Maybe<User> findUser(String id) {
    // Returns Just(user) if found, Nothing if not found
}

IO<User> loadUser(String id) {
    // Describes a side effect that will load the user when run
}

Why Effects Matter:

  • Make failure modes visible in type signatures
  • Enable composition of operations that may fail
  • Replace scattered try-catch blocks with unified error handling
  • Allow reasoning about code behaviour from types alone

Related: Effect Path, Railway-Oriented Programming


Effect Algebra

Definition: A sealed interface annotated with @EffectAlgebra where each permitted record represents a domain operation. The Java equivalent of "algebraic effects" from functional programming. Each operation carries its parameters and a continuation function that transforms the operation's natural result type.

Example:

@EffectAlgebra
public sealed interface ConsoleOp<A>
    permits ConsoleOp.ReadLine, ConsoleOp.PrintLine {

  <B> ConsoleOp<B> mapK(Function<? super A, ? extends B> f);

  record ReadLine<A>(Function<String, A> k) implements ConsoleOp<A> {
    @Override
    public <B> ConsoleOp<B> mapK(Function<? super A, ? extends B> f) {
      return new ReadLine<>(k.andThen(f));
    }
  }

  record PrintLine<A>(String message, Function<Unit, A> k) implements ConsoleOp<A> {
    @Override
    public <B> ConsoleOp<B> mapK(Function<? super A, ? extends B> f) {
      return new PrintLine<>(message, k.andThen(f));
    }
  }
}

When to Use: When you need to define a closed set of domain operations that can be interpreted in multiple ways (production, testing, audit). The sealed modifier guarantees exhaustive handling in interpreters.

Related: Continuation-Passing Style, Free Monad, @EffectAlgebra


Effect Path

Definition: The primary API in Higher-Kinded-J for working with effects. Effect Paths wrap core effect types (Either, Maybe, Try, IO, Validated) and provide a fluent, railway-oriented interface for composition. Each Path type offers consistent operations (map, via, recover, run) regardless of the underlying effect.

Available Path Types:

Path TypeUnderlying EffectPurpose
EitherPath<E, A>Either<E, A>Typed error handling
MaybePath<A>Maybe<A>Optional values
TryPath<A>Try<A>Exception handling
IOPath<A>IO<A>Deferred side effects
ValidationPath<E, A>Validated<E, A>Error accumulation
TrampolinePath<A>Trampoline<A>Stack-safe recursion
CompletableFuturePath<A>CompletableFuture<A>Async operations

Example:

// Create paths using the Path factory
EitherPath<Error, User> userPath = Path.either(findUser(id));
MaybePath<Config> configPath = Path.maybe(loadConfig());
TryPath<Data> dataPath = Path.tryOf(() -> parseJson(input));
IOPath<String> ioPath = Path.io(() -> readFile(path));

// All paths share the same fluent API
EitherPath<Error, String> result = userPath
    .map(User::name)                    // Transform success value
    .via(name -> validateName(name))    // Chain dependent operation
    .recover(err -> "Anonymous");       // Handle errors

// Execute and get result
String name = result.run().orElse("Unknown");

Related: Path, via, recover, Effect Path Documentation


Effect-Optics Bridge

Definition: The integration layer that connects Effect Paths with the Focus DSL, allowing seamless composition of effectful computations and immutable data navigation. The bridge enables focusing into data structures retrieved from effects and lifting optic operations into effectful contexts.

How It Works:

EFFECTS DOMAIN                    OPTICS DOMAIN
══════════════                    ═════════════
EitherPath<E, User>  ──┐    ┌──  FocusPath<User, Address>
TryPath<Config>      ──┤    ├──  Lens<Config, Settings>
IOPath<Data>         ──┘    └──  Traversal<Data, Item>
                        │  │
                        ▼  ▼
                   .focus(optic)
                        │
                        ▼
              UNIFIED COMPOSITION

Example:

// Fetch user (effect) then navigate to nested data (optics)
EitherPath<Error, String> city = userService.findById(userId)  // Effect: fetch
    .focus(UserFocus.address())                                 // Optics: navigate
    .focus(AddressFocus.city())                                 // Optics: deeper
    .map(String::toUpperCase);                                  // Transform

// Modify nested data within an effectful context
EitherPath<Error, User> updated = userService.findById(userId)
    .focusAndModify(
        UserFocus.address().andThen(AddressFocus.postcode()),
        postcode -> postcode.toUpperCase()
    );

// Combine multiple effect sources with optic navigation
EitherPath<Error, Report> report =
    Path.of(loadCompany(id))
        .focus(CompanyFocus.departments())     // Traverse to departments
        .via(dept -> loadMetrics(dept.id()))   // Effect for each
        .map(metrics -> generateReport(metrics));

Benefits:

  • No boilerplate for null checks during navigation
  • Optic failures (missing optional values) integrate with effect failures
  • Single vocabulary (map, via, focus) for both domains
  • Type-safe composition across effect and structure boundaries

Related: Effect Path, Focus DSL, FocusPath


@EffectAlgebra

Definition: An annotation processor that generates five classes per annotated sealed interface:

Generated ClassPurpose
*KindHKT marker + Witness
*KindHelperwiden/narrow conversions
*FunctorFunctor instance (delegates to mapK)
*OpsSmart constructors + Bound inner class
*InterpreterAbstract interpreter skeleton

When to Use: Annotate every sealed interface that defines a set of domain operations. The generated classes eliminate boilerplate and provide type-safe construction and interpretation.

Related: Effect Algebra, @ComposeEffects


EitherF

Definition: A sum type lifted to the type constructor level. Used to compose multiple effect algebras into a single combined type via right-nesting. The @ComposeEffects annotation generates this composition automatically.

Example:

// Right-nested composition of four effect algebras:
// EitherF<PaymentGatewayOp,
//   EitherF<FraudCheckOp,
//     EitherF<LedgerOp,
//       NotificationOp>>>
@ComposeEffects
public record PaymentEffects(
    Class<PaymentGatewayOp<?>> gateway,
    Class<FraudCheckOp<?>> fraud,
    Class<LedgerOp<?>> ledger,
    Class<NotificationOp<?>> notification) {}

When to Use: When your program uses operations from multiple effect algebras. @ComposeEffects generates the EitherF nesting, Inject instances, and a BoundSet automatically.

Related: Inject, Effect Algebra


foldMap

Definition: The method that interprets a Free monad program by traversing its instruction tree, applying a natural transformation (interpreter) to each Suspend node, and combining results using the target monad's flatMap. Stack-safe via internal trampolining.

Example:

var interpreter = Interpreters.combine(consoleInterp, dbInterp);
IO<String> result = IOKindHelper.IO_OP.narrow(
    program.foldMap(interpreter, Instances.monad(io())));

How It Works:

  • Pure(a) returns the value wrapped in the target monad
  • Suspend(instruction) applies the interpreter to the instruction
  • FlatMapped(program, continuation) interprets the sub-program, then flatMaps the continuation

Related: Free Monad, Interpreter


Free Monad

Definition: A data structure (Free<F, A>) that represents a program as a tree of instructions. There are five main node types: Pure (return a value), Suspend (an instruction to execute), FlatMapped (sequence two programs), HandleError (error recovery), and Ap (applicative sub-expression). Because the program is data, it can be inspected, transformed, and interpreted in different ways.

Example:

// Building a Free program from effect algebra operations
Free<G, String> program =
    console.readLine(Function.identity())
        .flatMap(name -> console.printLine("Hello, " + name, Function.identity())
        .flatMap(_ -> Free.pure("Done")));

// The program is a tree:
//   FlatMapped
//     Suspend[ReadLine]
//     λ(name) → FlatMapped
//                 Suspend[PrintLine("Hello, " + name)]
//                 λ(_) → Pure("Done")

When to Use: When you need to build programs that can be interpreted in multiple ways, inspected before execution, or composed from multiple effect algebras.

Related: foldMap, Effect Algebra, EitherF


Inject

Definition: A type class witnessing that one effect algebra F can be embedded into a composed effect type G (a right-nested EitherF chain). Generated by @ComposeEffects. Provides the inj method that wraps an instruction in the appropriate EitherF position.

When to Use: Automatically generated and used by the BoundSet smart constructors. You rarely interact with Inject directly.

Related: EitherF, BoundSet


Interpreter (Effect Handler)

Definition: A natural transformation that converts effect algebra instructions into a target monad (e.g., IO for production, Id for testing). Extends the abstract skeleton generated by @EffectAlgebra. Multiple interpreters are combined using Interpreters.combine().

Example:

public class IOConsoleInterpreter extends ConsoleOpInterpreter<IOKind.Witness> {
  @Override
  protected <A> Kind<IOKind.Witness, A> handleReadLine(ConsoleOp.ReadLine<A> op) {
    return IOKindHelper.IO_OP.widen(
        IO.delay(() -> op.k().apply(scanner.nextLine())));
  }

  @Override
  protected <A> Kind<IOKind.Witness, A> handlePrintLine(ConsoleOp.PrintLine<A> op) {
    return IOKindHelper.IO_OP.widen(
        IO.delay(() -> { System.out.println(op.message()); return op.k().apply(Unit.INSTANCE); }));
  }
}

When to Use: Write one interpreter per effect algebra per execution mode. A production interpreter targets IO; a test interpreter targets Id for pure, synchronous execution.

Related: foldMap, Effect Algebra


mapK

Definition: A method on each effect algebra record that composes the continuation function with a new transformation. The generated Functor delegates to mapK rather than using unsafe casts. Analogous to Stream.map but applied to a single instruction rather than a collection.

Example:

record ReadLine<A>(Function<String, A> k) implements ConsoleOp<A> {
  @Override
  public <B> ConsoleOp<B> mapK(Function<? super A, ? extends B> f) {
    return new ReadLine<>(k.andThen(f));  // Compose continuations
  }
}

Why It Matters: Enables the generated Functor to map over instructions without cast-through. Each record knows how to transform its own continuation, keeping type safety throughout the chain.

Related: Effect Algebra, Continuation-Passing Style


Path

Definition: The unified factory class for creating Effect Paths. Provides static methods to wrap values and computations in the appropriate Path type, serving as the main entry point to the Effect Path API.

Factory Methods:

// Maybe paths
Path.maybe(nullableValue)           // Wrap nullable, null becomes Nothing
Path.just(nonNullValue)             // Wrap known-present value
Path.nothing()                      // Empty MaybePath

// Either paths
Path.either(eitherValue)            // Wrap existing Either
Path.right(value)                   // Success EitherPath
Path.left(error)                    // Failure EitherPath
Path.of(nullableValue)              // Wrap nullable as EitherPath

// Try paths
Path.tryOf(() -> riskyOperation())  // Wrap exception-throwing code
Path.success(value)                 // Successful TryPath
Path.failure(exception)             // Failed TryPath

// IO paths
Path.io(() -> sideEffect())         // Wrap side-effecting code
Path.ioOf(value)                    // Pure value in IO context

// Validation paths
Path.valid(value)                   // Valid result
Path.invalid(error)                 // Invalid with error

Example:

// Building a complete workflow using Path factory
public EitherPath<OrderError, Receipt> processOrder(OrderRequest request) {
    return Path.maybe(customerRepository.find(request.customerId()))
        .toEitherPath(() -> new OrderError.CustomerNotFound())
        .via(customer -> Path.either(validateOrder(request, customer)))
        .via(validated -> Path.tryOf(() -> paymentService.charge(validated))
            .toEitherPath(OrderError.PaymentFailed::new))
        .map(payment -> createReceipt(request, payment));
}

Related: Effect Path, via, recover, Effect Path Documentation


ProgramAnalyser

Definition: A utility that traverses a Free monad program tree without executing it, counting instructions, error recovery points, and parallel scopes. All counts are lower bounds because FlatMapped continuations are opaque functions that cannot be inspected without a value.

Example:

ProgramAnalysis analysis = ProgramAnalyser.analyse(program);

analysis.suspendCount();     // Number of instructions
analysis.recoveryPoints();   // Number of HandleError nodes
analysis.parallelScopes();   // Number of Ap nodes
analysis.hasOpaqueRegions(); // FlatMapped continuations present

When to Use: Before executing programs in production, to estimate cost, count external calls, or verify structural properties. Useful for audit logging and capacity planning.

Related: Free Monad


Railway-Oriented Programming

Definition: A programming model where computations flow along two parallel tracks: a success track and a failure track. Operations automatically route values to the appropriate track, and failures propagate without manual checking at each step.

The Railway Metaphor:

SUCCESS TRACK  ═══════╦═══════╦═══════╦═══════► Result
                      ║       ║       ║
                   step 1  step 2  step 3
                      ║       ║       ║
FAILURE TRACK  ───────╨───────╨───────╨───────► Error

When a step succeeds, the value continues on the success track. When a step fails, execution switches to the failure track and subsequent steps are bypassed.

Example:

// Traditional approach: manual error checking at each step
User user = findUser(id);
if (user == null) return error("User not found");
Account account = getAccount(user);
if (account == null) return error("Account not found");
if (!account.isActive()) return error("Account inactive");
return success(account.getBalance());

// Railway-oriented: automatic track switching
EitherPath<Error, BigDecimal> balance =
    Path.of(findUser(id))
        .via(user -> getAccount(user))
        .via(account -> validateActive(account))
        .map(account -> account.getBalance());
// Failures propagate automatically; no manual checks needed

Key Operations:

  • map - Transform value on success track (stays on same track)
  • via (flatMap) - Chain to next operation that may switch tracks
  • recover - Switch from failure track back to success track

Benefits:

  • Eliminates nested if-else and try-catch pyramids
  • Business logic reads top-to-bottom
  • Error handling is consistent and composable
  • Type system ensures all failure cases are addressed

Related: Effect Path, via, recover


recover

Definition: The error recovery operation on Effect Paths, equivalent to handleErrorWith on MonadError. Allows switching from the failure track back to the success track by providing an alternative value or computation.

Signature: Path<E, A>.recover(Function<E, A> handler) → Path<E, A>

Example:

// Simple recovery with default value
EitherPath<Error, Config> config = loadConfig()
    .recover(error -> Config.defaults());

// Recovery that inspects the error
EitherPath<ApiError, User> user = fetchUser(id)
    .recover(error -> switch (error) {
        case NotFound _ -> User.guest();
        case RateLimited _ -> User.cached(id);
        default -> throw new RuntimeException(error);  // Re-throw unrecoverable
    });

// Recovery with a new Path (recoverWith)
EitherPath<Error, Data> data = primarySource()
    .recoverWith(error -> fallbackSource());  // Try alternative on failure

// Partial recovery - only handle specific errors
EitherPath<Error, Value> result = operation()
    .recover(error -> {
        if (error instanceof Retryable) {
            return retryOperation();
        }
        throw error;  // Propagate non-retryable errors
    });

When To Use:

  • Providing default values when operations fail
  • Implementing fallback strategies
  • Converting errors to success values
  • Selective error recovery based on error type

Related: Effect Path, Railway-Oriented Programming, via


via

Definition: The chaining operation on Effect Paths, equivalent to flatMap on monads. Applies a function that returns a new Path, allowing dependent operations to be sequenced. If the current Path is on the failure track, via is bypassed and the failure propagates.

Signature: Path<E, A>.via(Function<A, Path<E, B>> f) → Path<E, B>

Example:

// Each step depends on the previous result
EitherPath<Error, Order> orderPath =
    Path.of(userId)
        .via(id -> findUser(id))           // Returns EitherPath<Error, User>
        .via(user -> getCart(user))        // Returns EitherPath<Error, Cart>
        .via(cart -> validateCart(cart))   // Returns EitherPath<Error, ValidatedCart>
        .via(valid -> createOrder(valid)); // Returns EitherPath<Error, Order>

// Compare to map (which doesn't chain Paths):
EitherPath<Error, String> mapped = userPath.map(user -> user.name());
// map: A -> B (simple transformation)
// via: A -> Path<E, B> (operation that may fail)

When To Use:

  • Chaining operations where each step may fail
  • Sequencing dependent computations
  • Building pipelines of effectful operations

Contrast with map:

  • map(A -> B) - Transform the value, stay on same track
  • via(A -> Path<E, B>) - Chain to operation that may switch tracks

Related: Effect Path, Railway-Oriented Programming, recover


VResultPath

Definition: A first-class railway for VTask<Either<E, A>>: asynchronous work, run on a virtual thread, that can fail with a typed domain error E. It composes VTaskPath (async) and EitherPath (typed error) into one path, so neither Kind ceremony nor a hand-rolled EitherT bridge ever surfaces. It speaks the full family vocabulary (map/via/then, mapError/recover/recoverWith/bimap).

Example:

VResultPath<OrderError, OrderResult> process(OrderRequest request) {
    return Path.vresultDefer(() -> validateAddress(request.address()))  // VResultPath<OrderError, Address>
        .via(address -> reserveStock(address))                          // chain a fallible async step
        .recover(err -> OrderResult.rejected(err));                     // handle the typed Left
}

VTask<Either<OrderError, OrderResult>> carrier = process(req).run();    // execute on a virtual thread

Factories: Path.vresultRight/vresultLeft (decided), Path.vresultEither (lift a decided Either), Path.vresult (lift a VTask<Either<E, A>>), and Path.vresultDefer (defer the decision itself).

Two channels, kept apart: a business failure travels in the value channel as Left, whilst an unexpected defect stays on the VTask channel. The outcome-aware structured-concurrency combinators (firstSuccess, allSucceed, allSucceedAccumulating, withTimeout, bracketOutcome) preserve that split, and the with* resilience combinators are railway-aware.

Related: VResultPath, Path, Railway-Oriented Programming, Either, Resilience Combinators