Schema-backed data
Model and Msg become Effect schemas and TypeScript types. Message tags and payload fields survive generation.
--- layout: home title: Forma for Elm-style apps description: The Elm Architecture, checked from the database to the DOM. A prelude-defined Forma prototype generates Foldkit and Effect TypeScript, with exhaustive update, checked command recovery, and pure views. sidebar: false aside: false pageClass: forma-index forma-elm ---
Forma for Foldkit · pre-alpha prototype
A message arrives. Update chooses the next model and commands. View describes the screen. Forma gives that loop a small Lisp vocabulary and checks its contracts, then generates Foldkit on Effect 4. Exhaustive update, typed command recovery, and pure view work in the TypeScript prototype today. A single API contract from server to client is the next step.
message
│
▼
pure update
╱ ╲
model commands
│ │
▼ ▼
pure view Effect A E R
│ │
▼ ok + errors
DOM │
└──→ message
Prelude-defined vocabulary · TypeScript engine only · prototype reference
This embed runs the existing Effect dialect, showing the operation and recovery that underlie a command. Introduce an error to remove recovery and expose an unhandled typed failure, then restore it. The full msg / command / update / view / app prelude runs through generateElmProgram in a build script; it is not wired into the browser worker or language server.
00 The problem
Foldkit brings the Elm Architecture to Effect: schema-backed models, messages, commands, and pure update. TypeScript expresses those through schemas, unions, factories, and function signatures. Exhaustiveness depends on how you write the matcher. Forma makes the decisions visible as application vocabulary, so mistakes can point to the source you authored.
Messages and update
Adding a message creates an obligation.
msg declares a closed tagged union. Each update case returns (with next-model), optionally followed by command keywords. Adding a variant without an update case produces a located diagnostic. Wildcard update cases are rejected, so every message stays visible in review.
Message payloads are checked wherever they are built: command success, error recovery, and view events. A string cannot become GotCount.count. Update is pure; naming a command schedules work without running it.
with keeps the model and its work together. A keyword such as :load-count identifies a declared command; unknown command names are rejected. Ordinary if and let can choose transitions.
Commands
The Elm guarantee, checked.
Elm's Cmd has no typed failure channel. Here a command wraps an ordinary Forma Effect value with declared success, errors, and service requirements. :ok maps the value to a message. :error names each error and maps its payload to a message. The resulting command has E = never.
The checker requires a mapping for every error in the operation's declared set, even when a particular implementation never raises it. Catch-all handlers cannot erase that obligation. Services stay in R; TypeScript host code provides the resource layer to Foldkit.
This covers typed failures. Defects, interruption, and exceptions in hand-written service implementations are still Effect runtime concerns. The prototype does not prove totality or eliminate runtime crashes.
View
No services in render.
The Hiccup tree generates Foldkit HtmlBuilder calls. Text and class expressions are pure functions returning String. Click expressions are pure functions returning Msg. Calling a service or placing an Effect in either position fails the checker; view has no Effect environment.
The deliberately small surface supports :div, :span, :button, :class, and :on-click. A click contains a message value, evaluated from the current model when rendering. Arbitrary JavaScript handlers, raw HTML, and I/O are outside this vocabulary.
End to end
The database-to-DOM headline is the direction.
The HttpApi spike already checks server handler contracts against endpoint declarations. This prototype checks frontend operations and messages separately. It does not derive client commands from those endpoints or share their declarations across generators.
The proposed connection would derive client success and error messages from an api. Adding an endpoint error could then add a message variant and make frontend update non-exhaustive. Today you add the message and recovery explicitly; changing an API declaration alone does not change the app.
05 The output
This is the complete generated module from the counter source above, checked against foldkit@0.168.0 and effect@4.0.3. No Forma runtime is imported.
Model and Msg become Effect schemas and TypeScript types. Message tags and payload fields survive generation.
Prelude emit hooks produce Command.define. An adapter turns checked command keywords into deferred Foldkit commands.
The exported Counter configuration has Model, init, update, and view. Host code passes it, a container, and service layers to Runtime.makeElement.
Call generateElmProgram from @formalang/ts/elm. Prelude descriptors define the domain IR; existing mechanics checks the lowered expressions.
06 Evidence
The counter conformance case compares generated TypeScript to a golden, checks it under strict settings without type escapes, validates its declaration artifacts, and runs a headless harness.
These fixtures establish the prototype's current behavior. There is no browser DOM harness for the full DSL or generated HttpApi client.
07 The cost
This is a small application-language experiment. The costs and boundaries are part of its contract.
with. Every command error gets a named recovery mapping. The ceremony makes obligations reviewable.08 Open questions
The syntax is designed around authoring a Lisp application, rather than exposing every Foldkit factory.
A transition is a model plus work. with makes that one expression and keeps update pure. Named keywords avoid exposing Command construction in user code. Argument-bearing commands need a next design.
A vector tree is compact and familiar in a Lisp. Existing ui.lisp and ViewSpec describe ontology-bound interfaces; this view renders an application model. They remain separate prototypes, with no shared component or event bridge yet.
Model-driven subscriptions should describe lifetime and teardown, rather than act like commands fired by update. Their syntax and typed message relationship are proposed, not implemented.
Which endpoint errors become distinct messages? Which remain a shared failure payload? Until the HttpApi bridge answers that, the server and frontend contracts are checked independently.
09 Fit
The reference documents the complete prototype surface. The fixture shows the generated module and the service layers that exercise it.