Init Callable Architecture Proposal
Problem Statement
The current codebase has three different names for the initial state instance:
-
MODEL- Used in simple examples (README, verify_readme_usage) -
INITIAL- Used in fractal fragments (dashboard examples) -
init:- Runtime parameter for startup commands (different semantics)
Additionally, the planned v0.4.0 transition (INITIAL β Initial) would create visual collision with Model, where both look like type names despite having different purposes.
Current Fragment Pattern
module SystemInfo Model = Data.define(:output, :loading) INITIAL = Model.new(output: "Press 's'", loading: false) # Static constant UPDATE = ->(message, model) { ... } VIEW = ->(model, tui) { ... } end
Current Limitations
-
No parameterization: Child fragments canβt receive initialization props from parents
-
Naming confusion:
Model(type) vsINITIAL/MODEL(instance) vsinit:(command) -
Static only: Canβt dispatch initial commands alongside state
-
No context: Root fragments canβt access ARGV, ENV, or other runtime context
Proposed Solution
Replace the static INITIAL/MODEL constant with an Init callable that:
-
Returns
[model, command]- Same pattern asUpdate -
Accepts flags/props - Context from parent or runtime
-
Supports DWIM: Can return just
model(no command) likeUpdate
New Fragment Pattern
module SystemInfo Model = Data.define(:output, :loading) # Init receives flags from parent, returns [model, command?] Init = ->(disabled: false) do message = disabled ? "(disabled)" : "Press 's' for system info" [Model.new(output: message, loading: false), nil] end Update = ->(message, model) { ... } View = ->(model, tui) { ... } end
Root Fragment with Runtime Context
module App Model = Data.define(:user_name, :debug_mode, :data) Init = ->(argv:, env:) do debug = env['DEBUG'] == '1' name = argv[0] || env['USER'] || 'guest' model = Model.new(user_name: name, debug_mode: debug, data: nil) command = Command.http(:get, "/api/startup", :initial_data) [model, command] end Update = ->(message, model) { ... } View = ->(model, tui) { ... } end
Fractal Composition
module Dashboard Model = Data.define(:stats, :network, :theme) Init = ->(theme: :dark, env:) do # Parent can pass props to children stats_model, stats_cmd = StatsPanel::Init.(theme: theme) network_model, network_cmd = NetworkPanel::Init.(theme: theme) model = Model.new(stats: stats_model, network: network_model, theme: theme) command = Command.batch( Tea.route(stats_cmd, :stats), Tea.route(network_cmd, :network) ) [model, command] end Update = from_router View = ->(model, tui) { ... } end
Benefits
1. Eliminates Naming Confusion
| Current (3 names) | Proposed (1 name) |
|---|---|
Model (type) |
Model (type) |
INITIAL or MODEL (instance) |
Init (callable) |
init: (runtime param) |
init: (runtime param) |
2. Enables Parameterization (React-Style Props)
Parents can pass configuration to children:
# Parent decides child's theme, debug mode, etc. child_model, child_cmd = ChildFragment::Init.(theme: :light, debug: true)
3. Unifies Initialization Pattern
Both Init and Update now follow the same signature:
Init :: (flags) -> [Model, Command?] Update :: (Message, Model) -> [Model, Command?]
4. Supports Initial Commands
No need for separate init: runtime parameter pattern:
# Old way INITIAL = Model.new(data: nil) Tea.run(model: INITIAL, init: -> { fetch_data_command }) # New way Init = -> { [Model.new(data: nil), fetch_data_command] } Tea.run(fragment: MyApp) # Init is called automatically
5. Access to Runtime Context
Root fragments can inspect ARGV, ENV, config files, etc.:
Init = ->(argv:, env:) do config = parse_config(argv[0]) if argv[0] # ... end
Migration Path
Phase 1: Introduce Init alongside INITIAL
Both patterns work during transition:
# Old style (deprecated) INITIAL = Model.new(...) # New style (preferred) Init = -> { Model.new(...) }
Phase 2: Runtime Changes
# Current Tea.run(model: Fragment::INITIAL, view: Fragment::VIEW, update: Fragment::UPDATE) # Transitional (supports both) Tea.run(fragment: Fragment) # Calls Fragment::Init # OR Tea.run(model: initial_model, view: view, update: update) # Old style # Future Tea.run(fragment: Fragment, argv: ARGV, env: ENV)
Phase 3: DSL for Fractal Composition
Router could auto-call child Init:
module Dashboard include Tea::Router # Automatically calls StatsPanel::Init and NetworkPanel::Init mount :stats, fragment: StatsPanel, theme: :dark mount :network, fragment: NetworkPanel, theme: :dark Update = from_router end
Open Questions
1. Fragment Signature
What constants are required?
Option A: All four
Model, Init, Update, View # Complete fragment
Option B: Flexible
Model, Update, View # No Init = empty model Model, Init # View-less (backend fragment?)
2. Init Return Type DWIM
How flexible should the return be?
Init = -> { Model.new(...) } # Just model, no command Init = -> { [Model.new(...), nil] } # Explicit tuple Init = -> { [Model.new(...), some_command] } # With command
3. Backward Compatibility
Breaking or transitional?
-
Option A: v0.5.0 breaking change, remove
INITIALsupport entirely -
Option B: v0.4.x transitional, support both patterns with deprecation warnings
-
Option C: v0.4.x additive, keep
INITIALforever,Initis optional
4. Runtime API
How does Tea.run change?
# Current Tea.run(model: initial, view: view, update: update, init: startup_cmd) # Proposed Option 1: Fragment-first Tea.run(fragment: App, argv: ARGV, env: ENV) # Proposed Option 2: Hybrid Tea.run(fragment: App) # Uses App::Init # OR Tea.run(model: model, view: view, update: update) # Old style still works
5. Router DSL Integration
Should route :child, to: ChildFragment auto-initialize?
# Manual (explicit control) route :child, to: ChildFragment Init = ->(theme:) do child_model, child_cmd = ChildFragment::Init.(theme: theme) [Model.new(child: child_model), Tea.route(child_cmd, :child)] end # Automatic (magic convenience) mount :child, fragment: ChildFragment, theme: :dark # Auto-calls Init
Implementation Sketch
Runtime Changes
module RatatuiRuby::Tea def self.run(fragment: nil, model: nil, view: nil, update: nil, argv: [], env: {}) if fragment # New style: fragment-first init_result = fragment::Init.call(argv: argv, env: env) model, init_cmd = normalize_update_result(init_result) view = fragment::View update = fragment::Update else # Old style: explicit model/view/update # (backward compatible) end Runtime.run(model: model, view: view, update: update, init: init_cmd) end end
Fragment Helpers
module Tea::Fragment # Normalize Init or Update return values def self.call_init(fragment, **flags) result = fragment::Init.call(**flags) normalize(result) end def self.normalize(result) case result in [model, command] then [model, command] in model then [model, nil] end end end
Recommendation
I think this is a excellent direction because:
-
β Solves the naming collision completely
-
β Enables parent-to-child props (long-standing limitation)
-
β Unifies Init and Update patterns (both return tuples)
-
β Removes runtime context limitations (ARGV, ENV access)
-
β Simplifies the βwhatβs my initial command?β pattern
Suggested Approach
-
v0.4.x: Introduce
Initas optional, keepINITIALsupport -
Document the pattern with examples and migration guide
-
Add Router DSL sugar for
mount :child, fragment: ChildFragment, props... -
v0.5.0: Deprecate
INITIAL, makeInitrequired
This gives users time to migrate while immediately solving the parameterization problem for new code.
Next Steps
-
Get user feedback on this proposal
-
Prototype the runtime changes
-
Convert one example (fractal dashboard) to new pattern
-
Document the full API and migration path