Now I need to understand the relationship better. Let me check if there's any Lwd documentation in the opam switch. Let me run a command to see if we can find Lwd documentation. Actually, let me think about the explanation.
I'll structure the answer in three parts:
1. **Lwd.map in pure OCaml terms**: Explain it as a functor/monad for reactive computation, similar to how Option.map works but for time-varying values.
2. **Relationship with Nottui and Notty**: Nottui is a widget library built on Notty that uses Lwd for reactive UI composition. Notty provides low-level terminal drawing.
3. **Lwd with Eio fibers**: Lwd variables can be updated from any fiber via Lwd.set, and Lwd's evaluation is synchronous during stabilization phase.
Let me write this explanation. First, I need to ensure my understanding is correct. Let me think about the mental model:
- Lwd.t is a computation graph node that can be recomputed incrementally.
- Lwd.var is a mutable input node.
- Lwd.map creates a derived node that transforms its input.
- The graph is evaluated lazily when sampled (Lwd.quick_sample).
- Changes propagate through the graph.
Now for Eio: Since Lwd.set can be called from any fiber, and Lwd's graph evaluation is synchronous (not async/await), it works well with Eio. The render loop samples the Lwd graph periodically.
Let me write the answer.
Lwd.map in Pure OCaml Terms
Lwd.map is a pure functional transformation that creates a reactive dependency graph. Think of it as OCaml's List.map or Option.map, but for time-varying values.
Core Mental Model
(* Simplified type signature *)
val map : ('a -> 'b) -> 'a Lwd.t -> 'b Lwd.t
What it does:
- Takes a pure function
f: 'a -> 'b
- Takes a reactive value
x: 'a Lwd.t
- Returns a new reactive value
y: 'b Lwd.t where y = f(x) at all times
Key Characteristics (Pure OCaml Perspective)
- Referential Transparency:
Lwd.map f (Lwd.map g x) ≡ Lwd.map (f ∘ g) x
- No Side Effects: The function
f must be pure - no mutation, no I/O
- Lazy Evaluation: The function
f is only called when the result is needed
- Memoization: If
x hasn't changed, f(x) is not recomputed
- Incremental Computation: When
x changes, only the minimal work needed to update y is performed
Simple Example
(* Without Lwd - static computation *)
let x = 5
let y = x * 2 (* y = 10, forever *)
(* With Lwd - reactive computation *)
let x_var = Lwd.var 5
let y = Lwd.map (fun x -> x * 2) (Lwd.get x_var)
(* y is now a reactive value that's always 2× whatever x_var contains *)
Lwd.set x_var 7 (* y automatically becomes 14 *)
Implementation Sketch
Here's what Lwd.map might look like internally (conceptual):
module Lwd = struct
type 'a t = {
compute : unit -> 'a;
mutable cached : 'a option;
mutable version : int;
deps : dep list;
}
type 'a var = {
mutable value : 'a;
mutable version : int;
dependents : 'a t list;
}
let map f x =
let compute () = f (x.compute ()) in
{ compute; cached = None; version = 0; deps = [Dep x] }
end
Key Insight: Lwd.map doesn't compute anything immediately. It builds a dependency graph that will be evaluated later (during Lwd.quick_sample).
Relationship with Nottui and Notty
The Three-Layer Architecture
┌─────────────────────────────────────────┐
│ Nottui (Widgets) │ ← High-level, reactive UI components
│ uses Lwd.t │
├─────────────────────────────────────────┤
│ Lwd (Reactivity) │ ← Reactive computation layer
│ incremental values │
├─────────────────────────────────────────┤
│ Notty (Drawing) │ ← Low-level terminal graphics
│ images, attributes, etc. │
└─────────────────────────────────────────┘
How They Work Together
Notty - The Foundation
(* Notty deals with terminal graphics primitives *)
type image (* Grid of characters with attributes *)
type attr (* Colors, bold, underline, etc. *)
- Responsibility: Drawing pixels/characters to terminal
- Level: Direct terminal I/O, escape codes, resize handling
- Key Functions:
Notty_unix.Term.image, Notty_unix.Term.event
Lwd - The Reactive Engine
(* Lwd provides reactive values for UI state *)
let counter_var = Lwd.var 0
let counter_ui = Lwd.map (fun n ->
Nottui_widgets.string (string_of_int n)
) (Lwd.get counter_var)
- Responsibility: Manage changing values efficiently
- Level: Pure functional reactive programming
- Key Insight: Only recompute what changed
Nottui - The Widget Library
(* Nottui composes Lwd values into UI layouts *)
let ui = Lwd_utils.pack Ui.pack_y [
counter_ui;
button_ui;
]
- Responsibility: Layout, event handling, widget composition
- Level: Declarative UI programming
- Key Functions:
Ui.join_x, Ui.join_y, Ui.keyboard_area
Data Flow Example
(* 1. State in Lwd variable *)
let state_var = Lwd.var initial_state
(* 2. UI defined as Lwd.t of Notty images *)
let ui : Nottui.ui Lwd.t =
Lwd.map (fun state ->
(* Pure function from state to UI *)
render_tree state
) (Lwd.get state_var)
(* 3. Evaluation produces Notty image *)
let root = Lwd.observe ui
let current_image = Lwd.quick_sample root (* : Nottui.ui *)
(* 4. Nottui renders to Notty image *)
let notty_image : Notty.image = Renderer.render current_image
(* 5. Notty displays to terminal *)
Notty_unix.Term.image term notty_image
Lwd with Eio Fibers and Effects
The Concurrency Model
Eio provides cooperative multitasking with fibers, while Lwd provides synchronous reactive updates. They complement each other perfectly:
┌─────────────────────────────────────────┐
│ Eio Fibers (Concurrent) │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Input│ │Timer│ │Net │ │
│ │Fiber│ │Fiber│ │Fiber│ │
│ └─────┘ └─────┘ └─────┘ │
│ │ │ │ │
│ └─────────┼─────────┘ │
│ ▼ │
│ Lwd.set state_var │ ← Cross-fiber state updates
├─────────────────────────────────────────┤
│ Lwd Graph (Synchronous) │
│ ┌──────────┐ │
│ │ State │ │
│ │ Var │ │
│ └──────────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ UI │ │
│ │ (Lwd.map) │ │
│ └───────────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ Renderer │ │
│ └───────────┘ │
└─────────────────────────────────────────┘
Safe Cross-Fiber Updates
Key Property: Lwd.set is thread-safe and can be called from any Eio fiber.
(* Example: Background data fetching updating UI *)
Eio.Fiber.fork ~sw (fun () ->
while true do
(* 1. Background fiber runs concurrently *)
let new_data = fetch_latest_passwords () in
(* 2. Safely update Lwd variable from different fiber *)
Lwd.set passwords_var new_data;
(* 3. Sleep until next update *)
Eio.Time.sleep clock 30.0
done
)
(* Meanwhile, main render fiber samples the updated state *)
let render_loop () =
let ui = Lwd.quick_sample root in
Renderer.update renderer size ui
How Lwd Integrates with Eio's Effect System
- No Effect Conflicts: Lwd's computation is pure OCaml - no Eio effects needed
- Synchronous Evaluation:
Lwd.quick_sample runs synchronously in whatever fiber calls it
- Cancel-Safe: Lwd operations don't block, so fibers can be cancelled cleanly
(* Safe pattern: Eio fibers write, Lwd reads *)
let quit = Lwd.var false in
(* Input fiber *)
Eio.Fiber.fork ~sw (fun () ->
try
while not (Lwd.peek quit) do
let event = read_input () in
Lwd.set state_var (handle_input event (Lwd.peek state_var))
done
with Eio.Cancel.Cancelled _ ->
(* Clean cancellation - Lwd vars remain consistent *)
()
)
(* Render fiber - pure Lwd evaluation *)
Eio.Fiber.fork ~sw (fun () ->
while not (Lwd.peek quit) do
let ui = Lwd.quick_sample root in (* Pure computation *)
render ui;
Eio_unix.sleep 0.016 (* ~60 FPS *)
done
)
Critical Design Pattern: The Stabilization Loop
Your code uses this pattern:
let rec stabilize () =
let tree = Lwd.quick_sample root in
Renderer.update renderer size tree;
if Lwd.is_damaged root then stabilize () else image
Why this works with Eio:
Lwd.quick_sample evaluates the entire reactive graph synchronously
- Any
Lwd.set calls from other fibers mark the graph as "damaged"
- The stabilization loop re-evaluates until consistent
- Eio fibers can run concurrently without blocking this process
Performance Characteristics
| Operation |
Eio Fiber Safety |
Blocking |
Use Case |
Lwd.set |
✅ Thread-safe |
❌ Non-blocking |
Update state from any fiber |
Lwd.peek |
✅ Thread-safe |
❌ Non-blocking |
Read current value |
Lwd.quick_sample |
⚠️ Fiber-local |
❌ Non-blocking |
Evaluate graph in current fiber |
Lwd.is_damaged |
⚠️ Fiber-local |
❌ Non-blocking |
Check if graph changed |
Practical Example from Your Codebase
(* In ui_tree.ml - Keyboard handler called from input fiber *)
let keyboard_handler key =
match key with
| (`ASCII 'j', []) ->
(* Called from Eio input fiber *)
Lwd.set state_var (move_selection (Lwd.peek state_var) `Down);
`Handled
(* In main.ml - Render loop in separate fiber *)
let rec render_loop () =
if not (Lwd.peek quit) then (
render (); (* Calls Lwd.quick_sample internally *)
Eio_unix.sleep (1.0 /. 60.0);
render_loop ()
)
The Golden Rule
"Eio fibers for concurrency, Lwd for reactive state, Nottui for UI composition, Notty for rendering."
This separation of concerns allows:
- Responsive UI: Input handling never blocks rendering
- Efficient updates: Only changed UI parts recomputed
- Clean architecture: Pure view functions, effectful controllers
- Scalability: Add more background fibers without UI complexity
The combination is particularly powerful for TUIs because it provides React-like declarative UI programming with the performance and control of terminal applications.