In Bosatsu, IO[A] is a description of a computation that produces A.
Constructing an IO does nothing -- effects only execute when the runtime interprets the IO chain.
This makes side effects composable, testable, and statically analyzable.
No Side Effects At Construction Composable IO Chains Static Analysis
When you write state.write("hello") in Bosatsu, nothing happens.
You get back an IO[Unit] -- a data structure describing "write hello to this state."
The write does not execute until the runtime interprets the IO.
# This does NOT write anything. It creates a value.
def do_create(_: Unit) -> IO[Unit]: (
s <- io_state.flatMap()
s.write("fetch:start") # returns IO[Unit], not Unit
)
This is fundamentally different from JavaScript's setState("hello"),
which mutates immediately. In Bosatsu, effects are first-class values you can
pass around, compose, and analyze before execution.
useState setter calls are immediate mutations. There's no way to
inspect, compose, or delay a setState call -- once you call it, the mutation is queued.
In Bosatsu, IO values can be composed with flatMap and the full chain is visible
to the compiler before anything executes.
flatMap
flatMap sequences IO values. The result is a new IO value describing
"do A, then use A's result to build B, then do B."
The chain is a data structure the runtime walks through.
# Three IO operations chained together
def do_succeed(_: Unit) -> IO[Unit]: (
sn <- stage_name.flatMap() # 1. get state ref
_ <- sn.write("success").flatMap() # 2. write stage name
_ <- set_all_bg(...).flatMap() # 3. update colors
st.write("Step 3: completed") # 4. update status
)
Each <- binds the result of the previous IO, feeding it forward.
The entire chain is a single IO[Unit] value -- a recipe for the runtime to follow.
The demo visualizes the IO lifecycle as a pipeline of stage boxes.
Each stage uses style-background bindings -- the box colors
are reactive state, not hardcoded CSS.
# Each stage box has its own background color state
idle_bg: IO[State[String]] = state("#667eea")
created_bg: IO[State[String]] = state("#334155")
# The box reads its color from state
def stage_box(label: String, bg: String) -> VNode:
h("div", [
("style-background", bg) # bound to state!
], [text(label)])
When you click "Create IO," the handler writes new colors to each stage's
background state. The binding map dispatches these writes directly to the
DOM's style.background property -- no re-render, no diff.
idle_bg updates exactly one
style.background property on exactly one DOM element. O(1), no diffing.
Below the pipeline, the demo shows the growing IO chain as pseudocode.
This mirrors what the runtime actually sees: a chain of flatMap
calls building up a sequence of effects.
pure("fetch /api") → flatMap(httpGet) → flatMap(state.write)
Step 1: pure("fetch /api/data") : IO[String]
Step 2: flatMap(url -> httpGet(url)) : IO[Response]
Step 3: flatMap(resp -> state.write(resp.body)) : IO[Unit]
At each step, the IO chain grows. The runtime only executes when the full chain is assembled and returned to the event handler dispatch.
| Property | IO (Bosatsu) | Imperative (JS/React) |
|---|---|---|
| When effects run | Only when runtime interprets | Immediately on call |
| Composability | flatMap chains, first-class values | Callback nesting or async/await |
| Static analysis | Compiler sees all effects | Effects invisible to compiler |
| Testability | Inspect IO chain without executing | Must mock side effects |
| Provenance | Every write traced to its source | No built-in tracking |
IO-as-values is the foundation of Yichus's provenance tracking. Because every mutation is a described value, the compiler can trace which inputs produced which outputs -- enabling features like AI code detection and audit trails.
yichus matchless command now emits
<output>.matchless.analysis.json in addition to the
IR and metadata files. The sidecar includes: