Skip to content

Migrate Iso/Lens to STAB form (Iso<S,T,A,B>)#31

Merged
veewee merged 1 commit into
mainfrom
feature/stab-optics
May 27, 2026
Merged

Migrate Iso/Lens to STAB form (Iso<S,T,A,B>)#31
veewee merged 1 commit into
mainfrom
feature/stab-optics

Conversation

@veewee
Copy link
Copy Markdown
Owner

@veewee veewee commented May 26, 2026

Summary

Ports Iso and Lens from the two-template form (Iso<S, A> / Lens<S, A>) to the four-template profunctor "STAB" form (Iso<S, T, A, B> / Lens<S, T, A, B>), with:

  • Concrete Iso / Lens classes invariant on all four slots. This is where compose boundary detection lives — standard Psalm template inference catches mismatches via invariant unification, no plugin hack needed. The AdjacentTemplateValidator workaround from Fix compose() boundary detection under covariant Iso/Lens templates #30 is deleted.
  • IsoInterface / LensInterface covariant on all four slots. This is where storage flexibility lives — Iso<Person, Person, string, string> fits in IsoInterface<mixed, mixed, mixed, mixed> slots, so consumers can keep heterogeneous optics in a single collection without losing the leaf types. Same trade as C#'s IEnumerable<out T>.
  • Type-changing API exposed. Lens::set(S, B): T, Iso::from(B): T, Lens::update(S, callable(A): B): T. Type-changing optics like Lens<Person, AnonymizedPerson, string, HashedName> are now expressible.

The DynamicFunctionStorage compose() providers (Iso + Lens) updated to emit 2N+2 templates with STAB pairing.

New docs (docs/stab-optics.md) walk a reader unfamiliar with optics through the four template slots with concrete Person / AnonymizedPerson / HashedName examples, the cheat sheet, and a "Footnote: a note on variance" explaining the concrete-vs-interface split.

BC

Hard break on the type level. Every Iso<S, A> / Lens<S, A> docblock callsite breaks. Runtime is source-compatible — method bodies are unchanged.

Bumps to 1.0.0, so the 0.x line stays compatible for consumers who can't migrate yet.

Verified

  • composer cs:fix — clean
  • composer psalm --no-cache — green
  • vendor/bin/phpunit --no-coverage — 121 tests, 318 assertions
  • Broken-composition SA tests still trip Incompatible types found for T3/T4 via standard inference (confirmed by temporarily flipping the @psalm-suppress).
  • Cross-tested against a local php-soap/encoding branch consuming this; the interface-covariance choice unblocks its registry pattern without per-callsite casts.

Test plan

  • composer cs:fix && composer psalm && vendor/bin/phpunit --no-coverage all green
  • SA tests still catch boundary mismatches via invariant unification (no plugin hook)
  • New SA tests in tests/static-analyzer/{Iso,Lens}/compose_type_changing.php exercise non-trivial S ≠ T and A ≠ B
  • New unit tests exercise type-changing set / from end-to-end

@veewee veewee force-pushed the feature/stab-optics branch 2 times, most recently from 6166e93 to 7592b07 Compare May 26, 2026 12:29
@veewee veewee marked this pull request as draft May 26, 2026 12:40
@veewee veewee force-pushed the feature/stab-optics branch from 7592b07 to 8bf9aa5 Compare May 27, 2026 11:50
@veewee veewee marked this pull request as ready for review May 27, 2026 12:03
@veewee veewee requested a review from Copilot May 27, 2026 13:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the Iso and Lens optics from the 2-template form to the 4-template profunctor “STAB” form (<S, T, A, B>), enabling type-changing optics while relying on standard Psalm inference (via invariance) for compose-boundary detection, and removing the previous plugin workaround.

Changes:

  • Port Iso/Lens and their interfaces to STAB generics, including type-changing APIs (set(): T, from(): T, update(): T).
  • Update Psalm DynamicFunctionStorage compose providers to emit 2N+2 templates for STAB composition; remove AdjacentTemplateValidator.
  • Add/adjust unit tests, static-analyzer tests, and documentation to explain STAB and validate type-changing composition.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/unit/Lens/LensTest.php Adds a unit test covering type-changing Lens::set() behavior.
tests/unit/Iso/IsoTest.php Adds a unit test covering type-changing Iso::from() behavior.
tests/static-analyzer/Lens/compose.php Updates SA compose typing expectations to STAB templates and boundary-mismatch explanation.
tests/static-analyzer/Lens/compose_type_changing.php Adds SA test for type-changing Lens composition (S≠T, A≠B).
tests/static-analyzer/Iso/compose.php Updates SA compose typing expectations to STAB templates and boundary-mismatch explanation.
tests/static-analyzer/Iso/compose_type_changing.php Adds SA test for type-changing Iso composition (S≠T, A≠B).
src/Psalm/Lens/Provider/ComposeProvider.php Reworks compose signature generation to STAB chain (2N+2 templates).
src/Psalm/Iso/Provider/ComposeProvider.php Reworks compose signature generation to STAB chain (2N+2 templates).
src/Psalm/Compose/AdjacentTemplateValidator.php Removes the previous compose boundary validator workaround.
src/Lens/read_only.php Updates STAB templates for read_only() wrapper return type.
src/Lens/property.php Updates property() lens return type to STAB form.
src/Lens/properties.php Updates properties() lens typing to STAB and adjusts getter closure.
src/Lens/optional.php Updates optional() wrapper typing to STAB and to type-changing set (B→T).
src/Lens/LensInterface.php Expands interface to STAB templates and type-changing methods/compose signature.
src/Lens/Lens.php Expands concrete lens to STAB templates and updates method signatures accordingly.
src/Lens/index.php Updates index() lens return type to STAB form.
src/Lens/compose.php Updates compose function docs/typing to STAB.
src/Iso/object_data.php Updates object_data() iso/accessor typing to STAB form.
src/Iso/IsoInterface.php Expands interface to STAB templates and type-changing from()/compose signatures.
src/Iso/Iso.php Expands concrete iso to STAB templates and updates method signatures accordingly.
src/Iso/compose.php Updates compose function docs/typing to STAB.
README.md Links to new STAB optics walkthrough doc.
docs/stab-optics.md Adds new documentation explaining STAB optics, compose slot alignment, and variance tradeoffs.
docs/lens.md Adds cross-link to the new STAB walkthrough.
docs/isomorphisms.md Adds cross-link to the new STAB walkthrough.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Psalm/Lens/Provider/ComposeProvider.php
Comment thread src/Psalm/Iso/Provider/ComposeProvider.php
Comment thread src/Lens/properties.php
Comment thread src/Lens/compose.php Outdated
Comment thread src/Iso/compose.php Outdated
veewee added a commit that referenced this pull request May 27, 2026
- Guard Iso/Lens compose() type providers against zero-arg calls
  (range(1, 0) yielded [1, 0], producing a negative base offset and
  undefined $templates reads during Psalm analysis).
- Broaden compose() $lenses/$isos param from non-empty-array to array:
  compose is a monoid (identity() is the unit), so empty -> identity and
  single -> passthrough are correct results, and ...$maybeEmpty unpacking
  no longer needs a callsite guard.
- Add unit tests for single and empty composition (Iso + Lens).
Ports Iso and Lens from the two-template form to the four-template
profunctor STAB form (Iso<S,T,A,B> / Lens<S,T,A,B>):

- Concrete Iso/Lens invariant on all four slots (compose boundary
  detection via standard Psalm inference; AdjacentTemplateValidator removed).
- IsoInterface/LensInterface covariant on all four slots for storage flexibility.
- Type-changing API: Lens::set(S,B):T, Iso::from(B):T, Lens::update(S, callable(A):B):T.
- DynamicFunctionStorage compose() providers emit 2N+2 templates with STAB pairing,
  guarded against zero-arg calls.
- compose() $lenses/$isos param is array (not non-empty-array): compose is a
  monoid, so empty -> identity and single -> passthrough are valid results.
- New docs/stab-optics.md walkthrough; unit + SA tests including type-changing
  composition and single/empty composition.
@veewee veewee force-pushed the feature/stab-optics branch from bd09485 to 202a134 Compare May 27, 2026 13:43
@veewee veewee merged commit 876a9e9 into main May 27, 2026
13 of 14 checks passed
@veewee veewee deleted the feature/stab-optics branch May 27, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants