PCollections Optics

What You'll Learn

  • Which PCollections types @GenerateTraversals recognises 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 typeGeneratorReconstructionCardinality
PVector<A>PVectorGeneratorTreePVector.from(Collection)ZERO_OR_MORE
PStack<A>PStackGeneratorConsPStack.from(Collection)ZERO_OR_MORE
PSet<A>PSetGeneratorHashTreePSet.from(Collection)ZERO_OR_MORE
PSortedSet<A>PSortedSetGeneratorTreePSet.from(Comparator, Collection)ZERO_OR_MORE
PBag<A>PBagGeneratorHashTreePBag.from(Collection)ZERO_OR_MORE
PMap<K, V>PMapValueGeneratorHashTreePMap.from(Map) (value focus)ZERO_OR_MORE
PSortedMap<K, V>PSortedMapValueGeneratorTreePMap.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 @GenerateFocus navigator generation. The collection-like generators (PVector / PStack / PSet / PBag) widen through EachInstances.fromIterableCollecting, and PMap through EachInstances.mapValuesEachCollecting, the map-shaped companion to fromIterableCollecting. The sorted pair widen through fromIterableRebuilding and mapValuesEachRebuilding, which hand the rebuild the source as well, so it keeps the source's comparator. So @GenerateFocus on a record with a PMap or PSortedMap field produces a navigator returning a TraversalPath over 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 each Position in the PVector. After a focus change, TreePVector.from(...) rebuilds the persistent vector around the new elements.
  • Portfolio_exposureByCurrency() traverses each Double value in the PMap. 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 modify or modifyAll.
  • An element, or a key, needs no natural order of its own. A PSortedSet<Leaf> sorted by Comparator.comparing(Leaf::name) is traversed as a PSortedSet<String> is.
  • A sorted collection in natural order stays in natural order. PCollections' own report Comparator.naturalOrder() as their comparator. Another implementation of PSortedSet or PSortedMap may report null, as SortedSet allows, and the comparator is read through TreePSet.fromSortedSet or TreePMap.fromSortedMap, which take null as 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.


Key Takeaways

  • 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 from factory, so traversals round-trip cleanly through the persistent type
  • Sorted variants keep the source's comparator, under @GenerateTraversals and @GenerateFocus alike
  • Use EachInstances.fromIterableCollecting(TreePVector::from) (collections) or EachInstances.mapValuesEachCollecting(HashTreePMap::from) (maps) for hand-written optics outside @GenerateTraversals, or the Rebuilding forms for the sorted pair

See Also


Previous: PCollections Integration Next: Claude Code Skills