Focus DSL: Type Class and Effect Integration

What You'll Learn

  • Effectful modification with modifyF() using Applicative and Monad instances
  • Monoid-based aggregation with foldMap() on traversal paths
  • Generic collection traversal with traverseOver() for Kind<F, A> fields
  • Conditional modification with modifyWhen() and sum type access with instanceOf()
  • Path debugging with traced()
  • Bridging between Focus paths and Effect paths in both directions

Type Class Integration

The Focus DSL integrates deeply with Higher-Kinded-J type classes, enabling effectful operations, monoid-based aggregation, and generic collection traversal.

Effectful Modification with modifyF()

All path types support modifyF() for effectful transformations:

// Validation - accumulate all errors
Kind<ValidatedKind.Witness<List<String>>, User> result = userPath.modifyF(
    email -> EmailValidator.validate(email),
    user,
    ValidatedApplicative.instance()
);

// Async updates with CompletableFuture
Kind<CompletableFutureKind.Witness, Config> asyncResult = configPath.modifyF(
    key -> fetchNewApiKey(key),  // Returns CompletableFuture
    config,
    CompletableFutureApplicative.INSTANCE
);

// IO operations
Kind<IOKind.Witness, User> ioResult = userPath.modifyF(
    name -> IO.of(() -> readFromDatabase(name)),
    user,
    IOMonad.INSTANCE
);

Monoid-Based Aggregation with foldMap()

TraversalPath supports foldMap() for aggregating values:

// Sum all salaries using integer addition monoid
int totalSalary = employeesPath.foldMap(
    Monoids.integerAddition(),
    Employee::salary,
    company
);

// Concatenate all names
String allNames = employeesPath.foldMap(
    Monoids.string(),
    Employee::name,
    company
);

// Custom monoid for set union
Set<String> allSkills = employeesPath.foldMap(
    Monoids.set(),
    e -> e.skills(),
    company
);

Generic Collection Traversal with traverseOver()

When working with collections wrapped in Kind<F, A>, use traverseOver() with a Traverse instance:

// Given a field with Kind<ListKind.Witness, Role> type
FocusPath<User, Kind<ListKind.Witness, Role>> rolesPath = UserFocus.roles();

// Traverse into the collection
TraversalPath<User, Role> allRoles =
    rolesPath.<ListKind.Witness, Role>traverseOver(ListTraverse.INSTANCE);

// Now operate on individual roles
List<Role> roles = allRoles.getAll(user);
User updated = allRoles.modifyAll(Role::promote, user);

When to use traverseOver() vs each():

MethodUse Case
each()Standard List<T> or Set<T> fields
traverseOver()Kind<F, T> fields with custom Traverse
// For List<T> - use each()
TraversalPath<Team, User> members = TeamFocus.membersList().each();

// For Kind<ListKind.Witness, T> - use traverseOver()
TraversalPath<Team, User> members = TeamFocus.membersKind()
    .<ListKind.Witness, User>traverseOver(ListTraverse.INSTANCE);

Conditional Modification with modifyWhen()

Modify only elements that match a predicate:

// Give raises only to senior employees
Company updated = CompanyFocus.employees()
    .modifyWhen(
        e -> e.yearsOfService() > 5,
        e -> e.withSalary(e.salary().multiply(1.10)),
        company
    );

// Enable only premium features
Config updated = ConfigFocus.features()
    .modifyWhen(
        f -> f.tier() == Tier.PREMIUM,
        Feature::enable,
        config
    );

Working with Sum Types using instanceOf()

Focus on specific variants of sealed interfaces:

sealed interface Shape permits Circle, Rectangle, Triangle {}

// Focus on circles only
AffinePath<Shape, Circle> circlePath = AffinePath.instanceOf(Circle.class);

// Compose with other paths
TraversalPath<Drawing, Double> circleRadii =
    DrawingFocus.shapes()
        .via(AffinePath.instanceOf(Circle.class))
        .via(CircleFocus.radius());

// Modify only circles
Drawing updated = DrawingFocus.shapes()
    .via(AffinePath.instanceOf(Circle.class))
    .modifyAll(c -> c.withRadius(c.radius() * 2), drawing);

Path Debugging with traced()

Debug complex path navigation by observing values:

// Add tracing to see what values are accessed
FocusPath<Company, String> debugPath = CompanyFocus.ceo().name()
    .traced((company, name) ->
        System.out.println("Accessing CEO name: " + name + " from " + company.name()));

// Every get() call now logs the accessed value
String name = debugPath.get(company);

// For TraversalPath, observe all values
TraversalPath<Company, Employee> tracedEmployees = CompanyFocus.employees()
    .traced((company, employees) ->
        System.out.println("Found " + employees.size() + " employees"));

Bridging to Effect Paths

Focus paths and Effect paths share the same via composition operator but navigate different domains. The bridge API enables seamless transitions between them.

                    FOCUS-EFFECT BRIDGE

    ┌─────────────────────────────────────────────────────┐
    │                   Optics Domain                     │
    │  FocusPath<S, A> ───────────────────────────────────│
    │  AffinePath<S, A> ──────────────────────────────────│
    │  TraversalPath<S, A> ───────────────────────────────│
    └──────────────────────────┬──────────────────────────┘
                               │
                               │ toMaybePath(source)
                               │ toEitherPath(source, error)
                               │ toTryPath(source, supplier)
                               ▼
    ┌─────────────────────────────────────────────────────┐
    │                   Effects Domain                    │
    │  MaybePath<A> ──────────────────────────────────────│
    │  EitherPath<E, A> ──────────────────────────────────│
    │  TryPath<A> ────────────────────────────────────────│
    │  IOPath<A> ─────────────────────────────────────────│
    │  ValidationPath<E, A> ──────────────────────────────│
    └──────────────────────────┬──────────────────────────┘
                               │
                               │ focus(FocusPath)
                               │ focus(AffinePath, error)
                               ▼
    ┌─────────────────────────────────────────────────────┐
    │           Back to Optics (within effect)            │
    │  EffectPath<B> ─────────────────────────────────────│
    └─────────────────────────────────────────────────────┘

Direction 1: FocusPath to EffectPath

Extract a value using optics and wrap it in an effect for further processing:

// FocusPath always has a value, so these always succeed
FocusPath<User, String> namePath = UserFocus.name();
MaybePath<String> maybeName = namePath.toMaybePath(user);          // -> Just(name)
EitherPath<E, String> eitherName = namePath.toEitherPath(user);    // -> Right(name)
TryPath<String> tryName = namePath.toTryPath(user);                // -> Success(name)

// AffinePath may not have a value
AffinePath<User, String> emailPath = UserFocus.email();  // Optional<String> -> String
MaybePath<String> maybeEmail = emailPath.toMaybePath(user);        // -> Just or Nothing
EitherPath<String, String> eitherEmail =
    emailPath.toEitherPath(user, "Email not configured");          // -> Right or Left

Bridge Methods on FocusPath:

MethodReturn TypeDescription
toMaybePath(S)MaybePath<A>Always Just(value)
toEitherPath(S)EitherPath<E, A>Always Right(value)
toTryPath(S)TryPath<A>Always Success(value)
toIdPath(S)IdPath<A>Trivial effect wrapper

Bridge Methods on AffinePath:

MethodReturn TypeDescription
toMaybePath(S)MaybePath<A>Just if present, Nothing otherwise
toEitherPath(S, E)EitherPath<E, A>Right if present, Left(error) otherwise
toTryPath(S, Supplier<Throwable>)TryPath<A>Success or Failure
toOptionalPath(S)OptionalPath<A>Wraps in Java Optional effect

Bridge Methods on TraversalPath:

MethodReturn TypeDescription
toListPath(S)ListPath<A>All focused values as list
toStreamPath(S)StreamPath<A>Lazy stream of values
toMaybePath(S)MaybePath<A>First value if any

Direction 2: EffectPath.focus()

Apply structural navigation inside an effect context:

// Start with an effect containing a complex structure
EitherPath<Error, User> userPath = Path.right(user);

// Navigate within the effect using optics
EitherPath<Error, String> namePath = userPath.focus(UserFocus.name());

// AffinePath requires an error for the absent case
EitherPath<Error, String> emailPath =
    userPath.focus(UserFocus.email(), new Error("Email required"));

focus() Method Signatures:

Effect TypeFocusPath SignatureAffinePath Signature
MaybePath<A>focus(FocusPath<A, B>) -> MaybePath<B>focus(AffinePath<A, B>) -> MaybePath<B>
EitherPath<E, A>focus(FocusPath<A, B>) -> EitherPath<E, B>focus(AffinePath<A, B>, E) -> EitherPath<E, B>
TryPath<A>focus(FocusPath<A, B>) -> TryPath<B>focus(AffinePath<A, B>, Supplier<Throwable>) -> TryPath<B>
IOPath<A>focus(FocusPath<A, B>) -> IOPath<B>focus(AffinePath<A, B>, Supplier<RuntimeException>) -> IOPath<B>
ValidationPath<E, A>focus(FocusPath<A, B>) -> ValidationPath<E, B>focus(AffinePath<A, B>, E) -> ValidationPath<E, B>
IdPath<A>focus(FocusPath<A, B>) -> IdPath<B>focus(AffinePath<A, B>) -> MaybePath<B>

When to Use Each Direction

Use FocusPath to EffectPath when:

  • You have data and want to start an effect pipeline
  • Extracting values that need validation or async processing
  • Converting optic results into monadic workflows
// Extract and validate
EitherPath<ValidationError, String> validated =
    UserFocus.email()
        .toEitherPath(user, new ValidationError("Email required"))
        .via(email -> validateEmailFormat(email));

Use EffectPath.focus() when:

  • You're already in an effect context (e.g., after a service call)
  • Drilling down into effect results
  • Building validation pipelines that extract and check nested fields
// Service returns effect, then navigate
EitherPath<Error, Order> orderResult = orderService.findById(orderId);
EitherPath<Error, String> customerName =
    orderResult
        .focus(OrderFocus.customer())
        .focus(CustomerFocus.name());

Practical Example: Validation Pipeline

Combining both directions for a complete validation workflow:

// Domain model
record RegistrationForm(String username, Optional<String> email, Address address) {}
record Address(String street, Optional<String> postcode) {}

// Validation using Focus-Effect bridge
EitherPath<List<String>, RegistrationForm> validateForm(RegistrationForm form) {
    var formPath = Path.<List<String>, RegistrationForm>right(form);

    // Validate username (always present)
    var usernameValid = formPath
        .focus(FormFocus.username())
        .via(name -> name.length() >= 3
            ? Path.right(name)
            : Path.left(List.of("Username too short")));

    // Validate email if present
    var emailValid = formPath
        .focus(FormFocus.email(), List.of("Email required for notifications"))
        .via(email -> email.contains("@")
            ? Path.right(email)
            : Path.left(List.of("Invalid email format")));

    // Combine validations
    return usernameValid.via(u -> emailValid.map(e -> form));
}

See Also


Previous: Navigation and Composition Next: Custom Containers and Code Generation