Migrate Iso/Lens to STAB form (Iso<S,T,A,B>)#31
Merged
Conversation
6166e93 to
7592b07
Compare
4 tasks
3 tasks
7592b07 to
8bf9aa5
Compare
There was a problem hiding this comment.
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/Lensand their interfaces to STAB generics, including type-changing APIs (set(): T,from(): T,update(): T). - Update Psalm DynamicFunctionStorage compose providers to emit
2N+2templates for STAB composition; removeAdjacentTemplateValidator. - 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.
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.
bd09485 to
202a134
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports
IsoandLensfrom 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:Iso/Lensclasses 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. TheAdjacentTemplateValidatorworkaround from Fix compose() boundary detection under covariant Iso/Lens templates #30 is deleted.IsoInterface/LensInterfacecovariant on all four slots. This is where storage flexibility lives —Iso<Person, Person, string, string>fits inIsoInterface<mixed, mixed, mixed, mixed>slots, so consumers can keep heterogeneous optics in a single collection without losing the leaf types. Same trade as C#'sIEnumerable<out T>.Lens::set(S, B): T,Iso::from(B): T,Lens::update(S, callable(A): B): T. Type-changing optics likeLens<Person, AnonymizedPerson, string, HashedName>are now expressible.The DynamicFunctionStorage
compose()providers (Iso + Lens) updated to emit2N+2templates with STAB pairing.New docs (
docs/stab-optics.md) walk a reader unfamiliar with optics through the four template slots with concretePerson/AnonymizedPerson/HashedNameexamples, 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 the0.xline stays compatible for consumers who can't migrate yet.Verified
composer cs:fix— cleancomposer psalm --no-cache— greenvendor/bin/phpunit --no-coverage— 121 tests, 318 assertionsIncompatible types found for T3/T4via standard inference (confirmed by temporarily flipping the@psalm-suppress).php-soap/encodingbranch 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-coverageall greentests/static-analyzer/{Iso,Lens}/compose_type_changing.phpexercise non-trivialS ≠ TandA ≠ Bset/fromend-to-end