Optics - Basic Usage Examples
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 andset()
to perform an immutable update.
- Defining a nested data model (
// 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.
- Defining a
// 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 aList
or all values in aMap
. - Demonstrates:
- Using
@GenerateTraversals
to create optics for fields that are collections (List<Team>
,List<Player>
). - Composing a
Traversal
with anotherTraversal
and aLens
to create a single optic that focuses on a field within every element of a nested collection. - Using
modifyF()
with theId
monad to perform a pure, bulk update (e.g., adding bonus points to every player's score).
- Using
// 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 severalString
fields. - Using
@GenerateTraversals
with a customname
parameter to create a singleTraversal
that groups multiple fields (name
,email
,password
). - Using this traversal with
Validated
to run a validation function on each field. - Because
Validated
has anApplicative
that accumulates errors, the end result is aValidated
object containing either the original form or a list of all validation failures.
- Defining a
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 alookupUser
function that returns aValidated
type. This allows validating every member in the list. If any lookup fails, the entire operation results in anInvalid
.
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 withValidated
.
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 everyBoolean
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 thatmodifyF
only affects the value if theEither
is aRight
, leaving aLeft
untouched.
MaybeTraversalExample.java
- Demonstrates: Traversing a
Maybe<String>
field. - Scenario: A
Configuration
has an optionalproxyHost
. The traversal shows that an operation is only applied if theMaybe
is aJust
, leaving aNothing
untouched, which is analogous to theEither
example.
OptionalTraversalExample.java
- Demonstrates: Traversing a
java.util.Optional<String>
field. - Scenario: A
User
record has an optionalmiddleName
. The traversal is used to apply a function (liketoUpperCase
) 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 aTry
. The traversal allows modification of the value only if theTry
is aSuccess
, leaving aFailure
(containing an exception) unchanged.