Service Composition
As your game grows, you will likely need services that are shared across your init, update, and view functions: things like Networking, Leaderboards, or Save Data.
NOTE: the Env pattern here is runtime-agnostic; it applies verbatim to adaptive programs, with one improvement:
boot ctxreceives theGameContextbefore init runs, so context-dependent services initialize without special cases. See Adaptive Services.
Instead of passing these individually or relying on global state, create a strongly typed environment record.
The environment record
Initialize your services before you construct the program. This ensures they are ready immediately and avoids the "circular dependency" trap.
This pattern is often referred to as the Env (Environment) pattern in F# application architecture.
// The "Env" pattern
type Env = {
Network: INetworkService
Leaderboard: ILeaderboardService
}
// 1. Create environment independent of the program
let env = {
Network = Network.create "https://api.example.com"
Leaderboard = Leaderboard.create ()
}
// 2. Pass to program functions
// (either capture it, as here, or pass it as an argument)
let init = State.init env
let update = State.update env
Further Reading
For a deeper look at this pattern, we recommend Bartosz Sypytkowski's article: Dealing with complex dependency injection in F#
Avoiding Circular References
A common pitfall is trying to initialize a service inside your init function because it requires access to the GameContext or IAssets. This creates a circular dependency.
Guidance
- Prefer Independence: Design services to be independent of the concrete window or renderer if possible (they should live against
Mibo.Corecontracts likeGameContext/IAssetCache, not a backend host). -
Last resort: if you absolutely must have a circular reference, handle it carefully using:
- F#
refcells or mutable fields initialized later. - These are "you know what you are doing" scenarios; use them only when necessary.
- F#
_NOTE_: If you prefer to use a DI container, you can create it at the same time as you would with the environment and pass it to the program.
Full Program Example
Here is how the whole picture fits together in Program.fs.
module MyGame.Program
open Mibo.Elmish
open Mibo.Elmish.Graphics2D
// 1. Define the Environment Type
type Env = {
Network: INetworkService
Leaderboard: ILeaderboardService
}
// 2. Define the Composition Root (Factory)
let createEnv () =
{
Network = Network.create "https://api.example.com"
Leaderboard = Leaderboard.create ()
}
// 3. Define Game Logic (Dependencies Injected)
let init (env: Env) (ctx: GameContext) =
// Synchronous call (e.g. setting up listeners)
env.Network.Connect()
// Async call (e.g. fetching data)
let cmd = Cmd.ofAsync (async { return env.Leaderboard.Load() }) LeaderboardLoaded LeaderboardError
struct ({ Score = 0; HighScores = [] }, cmd)
let scoreSubmitted _ = ScoreSubmitted
let update (env: Env) (msg: Msg) (model: Model) =
match msg with
| ScoreChanged newScore ->
// Trigger async operation
let cmd = Cmd.ofAsync (async { return env.Leaderboard.SubmitScore(newScore) }) scoreSubmitted ScoreError
struct ({ model with Score = newScore }, cmd)
| LeaderboardLoaded scores ->
struct ({ model with HighScores = scores }, Cmd.none)
let view (env: Env) (ctx: GameContext) (model: Model) (buffer: RenderBuffer<RenderCmd2D>) =
// Draw score, etc.
()
// 4. Assemble & Run (Entry Point)
[<EntryPoint>]
let main _args =
// Create the environment FIRST
let env = createEnv ()
// partial application: fix the env argument of each function
let init = init env
let update = update env
let view = view env
// Compose the program with the Env captured
let configureGame (cfg: GameConfig) =
{ cfg with Title = "My Game"; Width = 1280; Height = 720 }
let createRenderer () = Renderer2D.create view
let program =
Program.mkProgram init update
|> Program.withConfig configureGame
|> Program.withAssets
|> Program.withRenderer createRenderer
// Run the game: use your backend's host
// raylib: new RaylibGame<Model, Msg>(program)
// MonoGame: new MiboGame<Model, Msg>(program)
let game = new RaylibGame<Model, Msg>(program)
game.Run()
0
Note on Async & The Game Loop
Common Cmd.ofAsync usage in Mibo follows standard Elmish rules: it does not block the game loop.
When you dispatch an async command:
1. The update function returns immediately with the new model.
2. The async work starts on a background thread (or the thread pool).
3. The game loop continues running (rendering frames, processing inputs).
4. When the async work completes, a new message is dispatched back into the loop.
This means you can safely perform heavy I/O (network requests, file saving) without causing frame stutters.
type EntryPointAttribute = inherit Attribute new: unit -> EntryPointAttribute
--------------------
new: unit -> EntryPointAttribute
Mibo