Optics - Basic Usage Examples

Info

This document provides a brief summary of the example classes found in the org.higherkindedj.example.optics package in the HKJ-Examples.

These examples showcase how to use the code generation features (@GenerateLenses, @GeneratePrisms, @GenerateTraversals) and the resulting optics to work with immutable data structures in a clean and powerful way.

LensUsageExample.java

This example is the primary introduction to Lenses. It demonstrates how to automatically generate Lens optics for immutable records and then compose them to read and update deeply nested fields.

  • Key Concept: A Lens provides a focus on a single field within a product type (like a record or class).
  • Demonstrates:
    • Defining a nested data model (League, Team, Player).
    • Using @GenerateLenses on records to trigger code generation.
    • Accessing generated Lenses (e.g., LeagueLenses.teams()).
    • Composing Lenses with andThen() to create a path to a deeply nested field.
    • Using get() to read a value and set() to perform an immutable update.
// Composing lenses to focus from League -> Team -> name
Lens<League, String> leagueToTeamName = LeagueLenses.teams().andThen(TeamLenses.name());

// Use the composed lens to get and set a value
String teamName = leagueToTeamName.get(league);
League updatedLeague = leagueToTeamName.set("New Team Name").apply(league);

PrismUsageExample.java

This example introduces Prisms. It shows how to generate optics for a sealed interface (a sum type) and use the resulting Prism to focus on a specific implementation of that interface.

  • Key Concept: A Prism provides a focus on a specific case within a sum type (like a sealed interface or enum). It succeeds if the object is an instance of that case.
  • Demonstrates:
    • Defining a sealed interface (Shape) with different implementations (Rectangle, Circle).
    • Using @GeneratePrisms on the sealed interface.
    • Using the generated Prism to safely "get" an instance of a specific subtype.
    • Using modify() to apply a function only if the object is of the target type.
  // Get the generated prism for the Rectangle case
  Prism<Shape, Rectangle> rectanglePrism = ShapePrisms.rectangle();
  
  // Safely attempt to modify a shape, which only works if it's a Rectangle
  Optional<Shape> maybeUpdated = rectanglePrism.modify(r -> new Rectangle(r.width() + 10, r.height()))
                                    .apply(new Rectangle(5, 10)); // Returns Optional[Rectangle[width=15, height=10]]
  
  Optional<Shape> maybeNotUpdated = rectanglePrism.modify(...)
                                       .apply(new Circle(20.0)); // Returns Optional.empty

TraversalUsageExample.java

This example showcases the power of composing Traversals and Lenses to perform bulk updates on items within nested collections.

  • Key Concept: A Traversal provides a focus on zero or more elements, such as all items in a List or all values in a Map.
  • Demonstrates:
    • Using @GenerateTraversals to create optics for fields that are collections (List<Team>, List<Player>).
    • Composing a Traversal with another Traversal and a Lens to create a single optic that focuses on a field within every element of a nested collection.
    • Using modifyF() with the Id monad to perform a pure, bulk update (e.g., adding bonus points to every player's score).
  // Compose a path from League -> each Team -> each Player -> score
  Traversal<League, Integer> leagueToAllPlayerScores =
      LeagueTraversals.teams()
          .andThen(TeamTraversals.players())
          .andThen(PlayerLenses.score());
  
  // Use the composed traversal to add 5 to every player's score
  var updatedLeague = IdKindHelper.ID.narrow(
      leagueToAllPlayerScores.modifyF(
          score -> Id.of(score + 5), league, IdentityMonad.instance()
      )
  ).value();

ValidatedTraversalExample.java

This example demonstrates a more advanced use case for Traversals where the goal is to validate multiple fields on a single object and accumulate all errors.

  • Key Concept: A Traversal can focus on multiple fields of the same type within a single object.
  • Demonstrates:
    • Defining a RegistrationForm with several String fields.
    • Using @GenerateTraversals with a custom name parameter to create a single Traversal that groups multiple fields (name, email, password).
    • Using this traversal with Validated to run a validation function on each field.
    • Because Validated has an Applicative that accumulates errors, the end result is a Validated object containing either the original form or a list of all validation failures.

Traversal Examples

These examples focus on using generated traversals for specific collection and container types, often demonstrating "effectful" traversals where each operation can succeed or fail.

ListTraversalExample.java

  • Demonstrates: Traversing a List<String> field.
  • Scenario: A Project has a list of team members. The traversal is used with a lookupUser function that returns a Validated type. This allows validating every member in the list. If any lookup fails, the entire operation results in an Invalid.

ArrayTraversalExample.java

  • Demonstrates: Traversing an Integer[] field.
  • Scenario: A Survey has an array of answers. The traversal is used with a validation function to ensure every answer is within a valid range (1-5), accumulating errors with Validated.

SetTraversalExample.java

  • Demonstrates: Traversing a Set<String> field.
  • Scenario: A UserGroup has a set of member emails. The traversal validates that every email in the set has a valid format (contains "@").

MapValueTraversalExample.java

  • Demonstrates: Traversing the values of a Map<String, Boolean> field.
  • Scenario: A FeatureToggles record holds a map of flags. The traversal focuses on every Boolean value in the map, allowing for a bulk update to disable all features at once.

EitherTraversalExample.java

  • Demonstrates: Traversing an Either<String, Integer> field.
  • Scenario: A Computation can result in a success (Right) or failure (Left). The traversal shows that modifyF only affects the value if the Either is a Right, leaving a Left untouched.

MaybeTraversalExample.java

  • Demonstrates: Traversing a Maybe<String> field.
  • Scenario: A Configuration has an optional proxyHost. The traversal shows that an operation is only applied if the Maybe is a Just, leaving a Nothing untouched, which is analogous to the Either example.

OptionalTraversalExample.java

  • Demonstrates: Traversing a java.util.Optional<String> field.
  • Scenario: A User record has an optional middleName. The traversal is used to apply a function (like toUpperCase) to the middle name only if it is present. This shows how to work with standard Java types in a functional way.

TryTraversalExample.java

  • Demonstrates: Traversing a Try<Integer> field.
  • Scenario: A NetworkRequest record holds the result of an operation that could have thrown an exception, wrapped in a Try. The traversal allows modification of the value only if the Try is a Success, leaving a Failure (containing an exception) unchanged.