Header menu logo Mibo

Commands

Commands handle side effects in Mibo's Elmish architecture. While update should be pure, commands let you execute impure work (I/O, timers, random) and dispatch messages back into the loop.

Quick Start

open Mibo.Elmish

type Msg =
  | SaveGame
  | SaveComplete
  | SaveFailed of exn

let update msg model =
  match msg with
  | SaveGame ->
    // Return unchanged model + command to save
    model, Cmd.ofAsync (saveToDisk model) SaveGameComplete SaveFailed

  | SaveComplete ->
    printfn "Game saved!"
    model, Cmd.none

Command Basics

Commands are values returned from init and update alongside the new model:

let update msg model : struct(Model * Cmd<Msg>) =
  match msg with
  | Tick ->
    { model with Time = model.Time + 1 }, Cmd.none
  | Fire ->
    model, Cmd.ofMsg SpawnProjectile

Cmd.none means "no side effects this frame."

Creating Commands

Immediate Messages

Dispatch another message right away:

Cmd.ofMsg SpawnProjectile

Async Workflows

Run F# async and map results:

let loadData url = async {
  use client = new HttpClient()
  let! json = client.GetStringAsync(url) |> Async.AwaitTask
  return parseJson json
}

// In update:
model, Cmd.ofAsync (loadData "api/data") DataLoaded LoadFailed

The async runs on a background thread. When it completes, the result message is dispatched back into the game loop.

.NET Tasks

For existing Task-based APIs:

let task = File.ReadAllTextAsync("save.json")
model, Cmd.ofTask task LoadComplete LoadFailed

Custom Effects

For full control over dispatch:

let delayedMsg (ms: int) (msg: Msg) : Cmd<Msg> =
  Cmd.ofEffect (Effect<Msg>(fun dispatch ->
    async {
      do! Async.Sleep ms
      dispatch msg
    } |> Async.StartImmediate
  ))

// Usage
model, delayedMsg 1000 (DelayedAction "1 second passed")

Combining Commands

Return multiple commands from one update:

let update msg model =
  match msg with
  | StartLevel level ->
    let newModel = { model with Level = level }
    let cmd = Cmd.batch [
      Cmd.ofMsg (PlayMusic level.Music)
      Cmd.ofAsync loadLevelData level.id LevelDataLoaded LoadFailed
      Cmd.ofMsg SpawnPlayer
    ]
    newModel, cmd

Function

Use Case

Cmd.batch [cmd1; cmd2; ...]

Variable list of commands

Cmd.batch2 (a, b)

Exactly 2 commands (optimized)

Cmd.batch3 (a, b, c)

Exactly 3 commands (optimized)

Cmd.batch4 (a, b, c, d)

Exactly 4 commands (optimized)

Deferred Commands

Sometimes you need to break infinite loops or schedule work for the next frame:

let update msg model =
  match msg with
  | CheckCondition ->
    if stillNeedToCheck then
      // Check again next frame, not immediately
      model, Cmd.deferNextFrame (Cmd.ofMsg CheckCondition)
    else
      model, Cmd.none

deferNextFrame prevents stack overflow when messages trigger themselves.

Parent-Child Composition

Child components often have their own message types. Map them to parent messages:

module Child =
  type Msg = Jump | Move
  let update msg model = ...

// Parent update:
let update msg model =
  match msg with
  | ChildMsg childMsg ->
    let (childModel, childCmd) = Child.update childMsg model.Child
    let parentModel = { model with Child = childModel }
    // Map child's Cmd<Child.Msg> to Cmd<Parent.Msg>
    parentModel, Cmd.map ChildMsg childCmd

Common Patterns

Fire-and-Forget

For effects where you don't care about the result:

let log msg =
  Cmd.ofEffect (Effect<_>(fun _ ->
    printfn "[LOG] %s" msg
  ))

// Usage
model, log "Player jumped"

Sequential Commands

Chain dependent operations:

let saveThenLoad path =
  Cmd.batch2 (
    Cmd.ofAsync saveData path (fun _ -> DataSaved) SaveFailed,
    Cmd.ofMsg LoadNextLevel  // Runs immediately, not waiting for save
  )

For true sequencing (B runs after A completes), use async:

let sequential = Cmd.ofAsync (async {
  do! saveDataAsync()
  let! result = loadDataAsync()
  return result
}) Loaded Failed

Conditional Commands

let maybeSave model =
  if model.Dirty then
    Cmd.ofAsync autoSave model SaveComplete SaveFailed
  else
    Cmd.none

// In update:
model, maybeSave model

Performance Notes

See Also

type Msg = | SaveGame | SaveComplete | SaveFailed of exn
type exn = System.Exception
val update: msg: Msg -> model: 'a -> 'a * 'b
val msg: Msg
val model: 'a
union case Msg.SaveGame: Msg
union case Msg.SaveFailed: exn -> Msg
union case Msg.SaveComplete: Msg
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val update: msg: 'a -> model: 'b -> struct ('b * 'c)
val msg: 'a
val model: 'b
val Tick: 'a
val Fire: 'a
val loadData: url: 'a -> Async<'b>
val url: 'a
val async: AsyncBuilder
val client: System.IDisposable
val json: obj
Multiple items
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * objnull -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...

--------------------
type Async<'T>
static member Async.AwaitTask: task: System.Threading.Tasks.Task -> Async<unit>
static member Async.AwaitTask: task: System.Threading.Tasks.Task<'T> -> Async<'T>
val task: obj
val delayedMsg: ms: int -> msg: Msg -> 'a
val ms: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
static member Async.Sleep: dueTime: System.TimeSpan -> Async<unit>
static member Async.Sleep: millisecondsDueTime: int -> Async<unit>
static member Async.StartImmediate: computation: Async<unit> * ?cancellationToken: System.Threading.CancellationToken -> unit
val update: msg: 'a -> model: 'b -> 'b * 'c
val level: obj
val newModel: 'b
val cmd: 'c
val CheckCondition: 'a
type Msg = | Jump | Move
val update: msg: 'a -> model: 'b -> 'c
val childMsg: obj
val childModel: obj
val childCmd: obj
module Child from commands
val parentModel: 'b
val log: msg: 'a -> 'b
val saveThenLoad: path: 'a -> 'b
val path: 'a
val sequential: obj

Type something to start searching.