Decision Trees
Three trees, one page
- Which optic type to choose for a given data shape and access pattern.
- Which API style (Focus DSL, manual composition, Fluent API, Free Monad DSL) to choose for a given task.
- Which advanced feature (filtered, indexed, profunctor) solves which specific problem.
The decision trees that appear in scattered form across the chapter intros are consolidated here. Use this page when you need to route quickly to the right tool.
Tree 1: Which optic do I need?
flowchart TD
Q{"What are you doing<br/>to the focus?"}
Q -->|"reading only"| R{"How many<br/>targets?"}
Q -->|"reading and writing"| M{"How many<br/>targets?"}
Q -->|"converting between<br/>equivalent types"| I(["Iso"])
R -->|"exactly one"| G(["Getter"])
R -->|"zero or more"| F(["Fold"])
M -->|"exactly one"| L(["Lens"])
M -->|"zero or one:<br/>the field may be absent"| A(["Affine"])
M -->|"zero or one:<br/>the value may be another variant"| P(["Prism"])
M -->|"zero or more"| T(["Traversal"])
classDef decision fill:#e5c890,stroke:#df8e1d,color:#232634
classDef tier fill:#a6d189,stroke:#40a02b,color:#232634
class Q,R,M decision
class I,G,F,L,A,P,T tier
Write-only access is the one case the tree does not reach: that is a Setter, and you arrive at it by knowing you never read.
| You have... | You want to... | Reach for |
|---|---|---|
| A required field on a record | Get and set | Lens |
| A variant of a sealed type | Match and modify the variant | Prism |
An optional field (nullable, Optional-wrapped) | Get and set if present | Affine |
| Two equivalent representations | Convert losslessly | Iso |
| A collection field | Apply an operation to every element | Traversal |
| A collection field, read-only | Query, search, aggregate | Fold |
| Read-only access to a single field | Get only | Getter |
| Write-only access, one or many targets | Set or modify, never read | Setter |
Tree 2: Which API style?
flowchart TD
S(["Start: Focus DSL<br/>CompanyFocus.headquarters().city()"]) --> Q{"Does the update<br/>need more?"}
Q -->|"no: plain nested update"| S2(["stay on the Focus DSL"])
Q -->|"it can fail, or accumulate errors"| FA(["Fluent API: OpticOps<br/>modifyEither, modifyAllValidated"])
Q -->|"it must be inspected,<br/>audited or run several ways"| FM(["Free Monad DSL<br/>see Advanced Optics"])
classDef decision fill:#e5c890,stroke:#df8e1d,color:#232634
classDef tier fill:#a6d189,stroke:#40a02b,color:#232634
classDef wire fill:#8caaee,stroke:#1e66f5,color:#232634
class Q decision
class S2,FA,FM tier
class S wire
| Your task | Use |
|---|---|
| Update a nested record field | Focus DSL |
| Compose optics across types you own | Focus DSL |
Validate as you modify (Either, Validated, Maybe) | Fluent API |
| Fan out an effect across a collection | Fluent API modifyAllF |
| Build optic operations as data, run later | Free Monad DSL |
| Audit trail of every optic operation | Free Monad DSL with logging interpreter |
| Reuse an optic for a type you cannot annotate | @ImportOptics or an OpticsSpec interface |
| Adapt an optic to a different data shape | Compose when the source nests, an Iso when the shapes are equivalent, Lens.of when it is lopsided, dimap when it is one-way |
Tree 3: Which advanced feature?
flowchart TD
Q{"What is the<br/>constraint?"}
Q -->|"only some elements<br/>should be touched"| F(["Filtered optics"])
Q -->|"the position matters<br/>as well as the value"| I(["Indexed optics"])
Q -->|"the source or target<br/>is the wrong shape"| P(["Profunctor optics"])
classDef decision fill:#e5c890,stroke:#df8e1d,color:#232634
classDef tier fill:#a6d189,stroke:#40a02b,color:#232634
class Q decision
class F,I,P tier
| Your problem | Reach for |
|---|---|
| "Apply only to elements matching a predicate" | Filtered Optics |
| "I need the index alongside each element" | Indexed Optics |
"Access by key in a Map" | Indexed Access: the At (full CRUD) and Ixed (read/update) type classes |
| "Apply over every element of a custom container" | Each Typeclass |
"Operate on individual characters of a String" | String Traversals |
| "Adapt a lens for a different source record type" | Compose: outerLens.andThen(innerLens) (Profunctor Optics) |
| "The two shapes hold the same information" | An Iso, then compose: Iso >>> Lens = Lens |
| "One-way conversion inside an effectful pipeline" | optic.dimap(...) (Profunctor Optics) |
| "Match a value by a predicate, not by type" | Prisms.nearly |
- Direction first, cardinality second. The tree asks what you are doing to the focus, then how many values it reaches; those two answers pick the optic between them.
- The two zero-or-one optics are not interchangeable. An
Affinereaches a value that may be absent; aPrismreaches a value that may be another variant, and can rebuild the structure from it. - Start on the Focus DSL and leave it only when forced. Failure, accumulation and effects move you to
OpticOps; inspection and multi-mode execution move you to the Free Monad DSL. - The advanced features are constraint-shaped. Subset means filtered, position means indexed, wrong shape means an adapter.
- These are entry points, not conclusions. Every leaf here has a page; the trees route, the pages decide.
- Optic Capabilities: what each optic can do once you have chosen one
- Composition Rules: what type results from composing two optics
- Annotations at a Glance: which annotation generates each optic
Previous: Production Readiness Next: Mapping at the Boundary