Skip to content

Row-to-row profunctors: Record/Variant merge directions, Shutter/Reel strengths, variant type sugar#53

Open
erykciepiela wants to merge 160 commits into
mainfrom
row-to-row-profunctors
Open

Row-to-row profunctors: Record/Variant merge directions, Shutter/Reel strengths, variant type sugar#53
erykciepiela wants to merge 160 commits into
mainfrom
row-to-row-profunctors

Conversation

@erykciepiela

Copy link
Copy Markdown
Collaborator

Summary

Builds a row-profunctor layer over Record/Variant on top of the core UI profunctor, replacing the earlier ChoiceLike/StrongLike experiments (both deleted).

Row profunctors (src/Data/Profunctor/Row/ + Row.purs)

Each of the four direction modules stacks three layers (merge → strength → single-field combinators):

  • RecordToRecord.purs (×→×) — merge recordToRecord; row-typed Strong (focusRecord on a sub-Record carrying the complement); introduceProperty/eliminateProperty/editProperty; default lifts withRecordDefault/withRecordOutputDefault
  • VariantToVariant.purs (+→+) — merge variantToVariant; row-typed Choice (focusVariant on a sub-Variant); eliminateCase/editCase
  • RecordToVariant.purs (×→+) — resolve, a loop/iteration step (Shutter: Either b c = Done/Loop); resolveProperty
  • VariantToRecord.purs (+→×) — retain, a Mealy/coroutine step (Reel: Tuple b c = output + next state); retainCase
  • Row.purs — umbrella Row class + Union-based widen/narrow reshapings

The mixed strengths deliberately have no (->) instance — a stateless function can't loop or retain state.

Forked compiler with variant sugar

Switches the toolchain to purescript-0.15.16-variant, adding [ case :: T, … ] variant type sugar and constructor sugar, adopted across library, tests, and docs. See doc/variant-sugar.md.

Showcase & examples

  • showcase/ — a realistic reactive checkout screen processed through all four merge directions: fake backend round-trip (request), cancel path, modal/notification, Shutter/Reel scenarios
  • src/Data/Profunctor/Row/Example.purs — widget vocabulary (textInput, checkbox, submit, …) at the row-profunctor level
  • New tests: BusinessOptics, EntityEventExample, HelloShutterReel, RestaurantReel

Docs

  • doc/row-profunctors.md — design write-up for the whole layer
  • doc/variant-sugar.md, CLAUDE.md

Net: +2371 / −1379 across 30 files, 154 commits.

🤖 Generated with Claude Code

erykciepiela and others added 30 commits June 8, 2026 19:23
… page

Make the V→V widgets (request/modal/notification) a request→response rename
(input case `from` → output case `to`, same payload), so submit → thankYou and
cancel → cancelled. The VariantToRecord stage then renders those into the result
page, and checkout resolves to
  Record (email, cardNumber, savePayment) -> Record (thankYou :: Record …, cancelled :: Record …)
— a checkout status / thank-you page carrying the placed (or cancelled) order.
Still annotation-free (only @l VTAs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ilure)

`request` drops its second (`@to`) type parameter: its response variant is left
deferred (forall w), so one request can resolve to several cases — here thankYou
or failure — inferred from the page's handlers. The checkout sends submit through
request alone (a deferred response can't share a V→V merge) and renders thankYou/
failure on the result page; a single signature pins the faked response payloads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…le action set

`request` becomes fully deferred on both sides (forall v w): the backend takes
whatever action variant comes in and answers with whatever response variant,
pinned to a concrete contract at the use site. The checkout adds a cancel button
(submit/cancel buttons merge), sends the action set through request, and renders
thankYou | failure | cancelled on the result page. The one use-site annotation on
request is its backend contract — it also fixes the button merge and the payloads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin statusBar/eventLog to a String-carrying case (Cons l String () r), so the
result page is a record of String status messages. The request contract returns
thankYou/failure/cancelled as Strings; checkout resolves to
  Record (email, cardNumber, savePayment) -> Record (thankYou::String, failure::String, cancelled::String).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`request` now processes only `submit` (→ thankYou | failure, deferred); `cancel`
is routed to a local `modal` in the same VariantToVariant.do merge and turned
straight into `cancelled` — it never reaches the backend. `modal`/`notification`
become payload-changing case transforms (from::a → to::b) so cancel's form maps
to a String message. Both V→V handlers are pinned to their contract (which also
fixes the button merge); the page resolves to thankYou/failure/cancelled Strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `actionButton` (Record () -> Variant): reads nothing, fires a case with an
empty Record () payload. Use it for cancel, so cancel :: Record () instead of the
whole form; modal maps cancel(Record ()) -> cancelled(String).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…type args)

Drop the @from/@to Cons-label params from modal and notification; they're now
forall v w (like request), so a use-site annotation pins them with no visible
type arguments. checkout's modal loses its @"cancel" @"cancelled" VTAs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Resolving/Retaining instances to the fake carrier and two signed
checkout-domain scenarios — confirmPayment (Shutter, resolve, ×→+ loop step)
and runningTotal (Reel, retain, +→× Mealy step) — making Done/Loop and
output+state visible in the types. Document both in the showcase README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ages

Place the mixed strengths inside the direction-matching primitives: submit
is now a Shutter (shutter, ×→+ with an editing loop case) and statusBar/
eventLog are Reels (reel, +→× retaining state). checkout uses them, so
Shutter/Reel are incorporated without artificial stages. request/modal stay
+→+ (neither). Drop the orphan confirmPayment/runningTotal bindings; update
README to explain the direction constraint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
submit now takes its Done/Loop case labels as visible type params
(submit @done @loop), closing the output row via a Cons chain so the merge
stays unambiguous. checkout uses submit @"submit" @"editing"; README updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- bump purs to 0.15.16-variant.2 (the [ ... ] sugar → Prim.Variant.Variant)
- point variant dep at local fork where Data.Variant.Variant = Prim.Variant.Variant
- rewrite checkout's VariantToVariant annotations to [ ... ], dropping the Data.Variant import

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o .5

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use the variant compiler's `[ … ]` variant sugar and PureScript's `{ … }`
record sugar uniformly for all row types:

  Record (a :: T) -> { a :: T }    Variant (a :: T) -> [ a :: T ]
  Record r        -> { | r }       Variant r        -> [ | r ]
  Record ()       -> {}            Variant ()       -> []

Covers the four row-profunctor direction modules, Row.purs, Default.purs,
Example.purs, Commons.purs, Data.Default, Web.purs, and doc/row-profunctors.md
(prose type expressions and code fences). Drops now-unused `Variant` imports.
Class names, method names, and qualified `Record.*` refs are left untouched.

Also delete the stale showcase/README.md (referenced a deleted Logic.purs).

spago build succeeds with no warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gar in tests

Add doc/variant-sugar.md describing the forked purs (0.15.16-variant.6) and its
three variant sugar forms (type [ … ], constructor .label, pattern case _ of
.label), including why purescript-variant is still required (the fork is purely
syntactic) and where the value sugar applies (literal labels only; total case_
eliminators kept for exhaustiveness). Link it from README and add a syntax note
to row-profunctors.md.

Adopt constructor sugar in concrete test code: inj (Proxy @"lit") x -> .lit x in
test/Main.purs and test/RestaurantReel.purs (dropping now-unused inj/Proxy
imports). Total case_ # on eliminators left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… imports

Replace every `Variant ( … )` type annotation in the test suite with the fork's
[ … ] sugar (= Prim.Variant.Variant), eliminating the last project-wide
references to the Data.Variant.Variant type name. test/Main.purs and
test/BusinessOptics.purs no longer import Data.Variant at all — Eq/Show
instances for assertEqual resolve transitively. RestaurantReel/EntityEventExample
keep only (case_, on) for their total eliminators.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n tests

The earlier text claimed only the type sugar [ … ] was used and the
constructor/pattern forms had no applicable site. That contradicts the later
adoption of .label construction in test/Main.purs and test/RestaurantReel.purs
and the project-wide elimination of the Variant type name. Replace with a
per-form adoption table reflecting the actual state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ithRecordOutputDefault

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… naming in demo

Commons.purs sticks to the naming convention: case for variants
(case_ / case_', since `case` is reserved), property for records.
demo/1/Model.purs still referenced the long-gone `field` lens
(unnoticed because demo/ is outside spago sources) — now `property`.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
… transformers, drop unlawful case_'

case_ and property now state p a a -> p [ | s ] [ | s ] / p { | s } { | s }
directly instead of the Prism/Lens synonyms. case_' removed: unused, and
its always-succeeding fallback broke the prism round-trip law.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
…rection modules

editProperty/editCase and Commons' property/case_ were the same functions
under two names. Single canonical home now: property in RecordToRecord,
case_ in VariantToVariant, both in explicit Strong/Choice transformer form.
Commons keeps only its Maybe/Either helpers; input delegates to property.
EntityEventExample qualifies Data.Variant.case_ to avoid the new clash.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
…erty, type-changing property/case_

propertyToCase (on resolve) and caseToProperty (on retain) are the mixed-
direction analogues of property/case_: the wrapped p a b runs on the focused
label l, the leftover row crosses the x/+ boundary wrapped under case/field w.
property/case_ generalized to type-changing p a b -> p s t (prop was natively
so; case_ rebuilt on prismE), subsuming lensProperty/prismCase — deleted.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
…ant/recordToCase/caseToRecord

The whole-row-at-one-end family, named by what the wrapped profunctor
consumes -> produces (record/variant = whole row, property/case = single
label): introduceProperty -> recordToProperty (desugared to explicit
transformer form), case_' -> caseToVariant (now takes p a [ | t ] — g
dispatches into the output variant itself, dropping the @w parameter),
plus new recordToCase (x->+, plain rmap (inj l) — the introduceCase the
diagonals can't have) and caseToRecord (+->x, Mealy reducer on Retaining).
eliminateCase removed: it is caseToVariant with a Void-pinned sink.
introduceProperty/eliminateProperty/eliminateCase constraints minimized
to Strong/Choice before the rename (bodies never used the row classes).

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
…r the strengths

StrongRecordToRecord/ChoiceVariantToVariant/ResolvingRecordToVariant/
RetainingVariantToRecord each had a single universal catch-all instance,
which forecloses carrier overrides — a class with no instance diversity
and no abstraction is a function in disguise. focusRecord/focusVariant
are now plain functions over Strong/Choice; shutterWrap/reelWrap over
Resolving/Retaining, gaining visible @w application (Proxy argument
dropped — the instance-method limitation is gone with the class).
Each direction module is now two layers: merge class + bare strength.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
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