PCollections Optics
- Which PCollections types
@GenerateTraversalsrecognises automatically - How generated traversals reconstruct persistent collections after a focus change
- The Focus DSL navigation patterns for PCollections fields
- How the sorted variants keep their comparator
PCollections persistent collections are first-class citizens of the optics generator system. Annotating a record whose components are PVector, PStack, PSet, PSortedSet, PBag, PMap, or PSortedMap produces traversal code that round-trips through the persistent type, with no production-code changes required.
This page documents the seven PCollections generator plugins. For the underlying compatibility hypothesis and benchmark numbers, see PCollections Integration.
Supported Types
| Field type | Generator | Reconstruction | Cardinality |
|---|---|---|---|
PVector<A> | PVectorGenerator | TreePVector.from(Collection) | ZERO_OR_MORE |
PStack<A> | PStackGenerator | ConsPStack.from(Collection) | ZERO_OR_MORE |
PSet<A> | PSetGenerator | HashTreePSet.from(Collection) | ZERO_OR_MORE |
PSortedSet<A> | PSortedSetGenerator | TreePSet.from(Comparator, Collection) | ZERO_OR_MORE |
PBag<A> | PBagGenerator | HashTreePBag.from(Collection) | ZERO_OR_MORE |
PMap<K, V> | PMapValueGenerator | HashTreePMap.from(Map) (value focus) | ZERO_OR_MORE |
PSortedMap<K, V> | PSortedMapValueGenerator | TreePMap.from(Comparator, Map) (value focus) | ZERO_OR_MORE |
Map generators focus on the value type (index 1). Keys are passed through unchanged.
All seven generators run at the default priority and activate automatically once org.pcollections:pcollections is on your annotation processor's classpath.
All seven generators participate in
@GenerateFocusnavigator generation. The collection-like generators (PVector/PStack/PSet/PBag) widen throughEachInstances.fromIterableCollecting, andPMapthroughEachInstances.mapValuesEachCollecting, the map-shaped companion tofromIterableCollecting. The sorted pair widen throughfromIterableRebuildingandmapValuesEachRebuilding, which hand the rebuild the source as well, so it keeps the source's comparator. So@GenerateFocuson a record with aPMaporPSortedMapfield produces a navigator returning aTraversalPathover the values.
Worked Example
Annotate a record whose components include PCollections types:
import org.higherkindedj.optics.annotations.GenerateFocus;
import org.higherkindedj.optics.annotations.GenerateTraversals;
import org.pcollections.PVector;
import org.pcollections.PMap;
@GenerateFocus
@GenerateTraversals
public record Portfolio(
String owner,
PVector<Position> positions,
PMap<String, Double> exposureByCurrency) {}
The processor emits a PortfolioTraversals companion class with two traversals:
Portfolio_positions()traverses eachPositionin thePVector. After a focus change,TreePVector.from(...)rebuilds the persistent vector around the new elements.Portfolio_exposureByCurrency()traverses eachDoublevalue in thePMap. Keys are preserved, values are replaced;HashTreePMap.from(...)rebuilds the persistent map.
The Focus DSL composes these into navigators with the correct path types automatically:
import static com.example.PortfolioFocus.portfolio;
TraversalPath<Portfolio, Position> allPositions = portfolio().positions();
TraversalPath<Portfolio, Double> allExposures = portfolio().exposureByCurrency();
TraversalPath is selected because each generator declares Cardinality.ZERO_OR_MORE and these fields are widened, by widenCollections = true on the record, or by widenFields naming the field. Left un-widened, a ZERO_OR_MORE container keeps the container itself in focus, and .each(...) widens it at the call site.
Reconstruction Cost
Generated traversals copy the persistent collection into a JDK ArrayList for traversal, then hand the resulting list back to the persistent factory. That gives O(n) iteration plus the construction cost of the persistent type, which for TreePVector is O(n) and for tree-backed sets and maps is O(n log n). For most domain models this is invisible alongside the work the focus function does.
If you are operating on a hot path and the persistent type matters more than the optic structure, prefer hand-written code that uses PCollections' native with/plus operations rather than going through @GenerateTraversals. The generated traversal is correct, not minimal.
Sorted Variants and Comparators
PSortedSet and PSortedMap are reconstructed with the source's comparator, through TreePSet.from(Comparator, Collection) and TreePMap.from(Comparator, Map). A generated traversal and a Focus path both keep the order the source keeps:
- A set or map in a custom order stays in it after
modifyormodifyAll. - An element, or a key, needs no natural order of its own. A
PSortedSet<Leaf>sorted byComparator.comparing(Leaf::name)is traversed as aPSortedSet<String>is. - A sorted collection in natural order stays in natural order. PCollections' own report
Comparator.naturalOrder()as their comparator. Another implementation ofPSortedSetorPSortedMapmay reportnull, asSortedSetallows, and the comparator is read throughTreePSet.fromSortedSetorTreePMap.fromSortedMap, which takenullas natural order.
One shape is refused: a wildcard where the container is ordered, such as PSortedSet<? extends Leaf> or PSortedMap<? extends String, Integer>. The source's comparator then compares an unknown type, so a rebuild that reuses it would not compile. @GenerateTraversals notes the component and generates no traversal for it, and @GenerateFocus rejects it where widenCollections or widenFields widens it. Declare the component with concrete type arguments, such as PSortedSet<Leaf>. @GenerateTraversals traverses a wildcard in a sorted map's value, PSortedMap<String, ? extends Leaf>, as in any other map.
Hand-Written Optics
If you need an optic outside @GenerateTraversals (for instance, on a third-party type you cannot annotate), the same building blocks are available:
import org.higherkindedj.optics.each.EachInstances;
import org.higherkindedj.optics.Each;
import org.pcollections.PVector;
import org.pcollections.TreePVector;
import org.pcollections.PMap;
import org.pcollections.HashTreePMap;
import org.pcollections.PSortedSet;
import org.pcollections.TreePSet;
Each<PVector<String>, String> pvectorEach =
EachInstances.fromIterableCollecting(TreePVector::from);
// Persistent maps use the map-shaped companion helper.
Each<PMap<String, Integer>, Integer> pmapValues =
EachInstances.mapValuesEachCollecting(HashTreePMap::from);
// A sorted set hands its comparator on, through the form that also receives the source.
Each<PSortedSet<String>, String> psortedSetEach =
EachInstances.fromIterableRebuilding(
(source, list) -> TreePSet.from(source.comparator(), list));
fromIterableCollecting accepts any function that builds a persistent collection from a List; mapValuesEachCollecting does the same for any java.util.Map subtype, rebuilding the persistent map from a JDK Map. fromIterableRebuilding and mapValuesEachRebuilding hand the function the source as well, which is how a sorted collection keeps its comparator. The same forms cover all the PCollections types. There is no library-specific factory class; the generic helpers are the public API.
When PCollections Is the Right Choice
PCollections excels when you want persistent immutable collections through the standard java.util interfaces, with no custom typeclass machinery. The optics generators slot it into Higher-Kinded-J's traversal pipeline so domain records using PVector, PMap, and friends compose with the rest of the optics ecosystem without any glue code.
For projects already on Eclipse Collections or Vavr, those existing generators remain the better fit. PCollections is most attractive for greenfield projects that want a small dependency footprint and java.util compatibility.
- Seven generator plugins cover every PCollections collection and map type
- Activation is automatic once PCollections is on the annotation processor classpath
- Reconstruction goes through each type's
fromfactory, so traversals round-trip cleanly through the persistent type - Sorted variants keep the source's comparator, under
@GenerateTraversalsand@GenerateFocusalike - Use
EachInstances.fromIterableCollecting(TreePVector::from)(collections) orEachInstances.mapValuesEachCollecting(HashTreePMap::from)(maps) for hand-written optics outside@GenerateTraversals, or theRebuildingforms for the sorted pair
- Traversal Generator Plugins - The full SPI catalogue and architecture
- PCollections Integration - compatibility tests, benchmarks, and the underlying
ListKindstory - Focus DSL - Composing generated traversals into navigators
Previous: PCollections Integration Next: Claude Code Skills