tiramisu/effect

Side effects for Tiramisu applications.

Effects are data descriptions of side effects to perform. They’re returned from your update function and executed by the runtime after state updates. This keeps your game logic pure and testable.

Common Effects

Game Loop Pattern

The typical game loop uses effect.dispatch(Tick) to request the next frame:

fn update(model, msg, ctx) {
  case msg {
    Tick -> {
      let new_rotation = model.rotation +. duration.to_seconds(ctx.delta_time)
      #(Model(rotation: new_rotation), effect.dispatch(Tick), option.None)
    }
  }
}

Types

Opaque effect type that can dispatch messages back to the application.

Effects are data descriptions of side effects to perform. The runtime executes them after your update function returns.

pub opaque type Effect(msg)
pub type TimerId =
  global.TimerID

Values

pub fn batch(effects: List(Effect(msg))) -> Effect(msg)

Batch multiple effects to run them together.

All effects execute in order during the same frame.

Example

effect.batch([
  effect.dispatch(NextFrame),
  play_sound_effect("jump.wav"),
  update_scoreboard(score),
])
pub fn cancel_interval(id: global.TimerID) -> Effect(msg)

Cancel a recurring interval by its ID.

Pass the interval ID that was dispatched via on_created when you created the interval.

Example

case model.spawn_interval {
  option.Some(id) -> effect.cancel_interval(id)
  option.None -> effect.none()
}
pub fn clipboard_read(
  on_success on_success: fn(String) -> msg,
  on_error on_error: msg,
) -> Effect(msg)

Read text from the system clipboard.

Must be called in response to user interaction due to browser security.

Example

type Msg {
  PasteClicked
  ClipboardData(String)
  PasteFailed
}

fn update(model, msg, ctx) {
  case msg {
    PasteClicked -> #(
      model,
      effect.clipboard_read(
        on_success: ClipboardData,
        on_error: PasteFailed,
      ),
    )
    ClipboardData(text) -> {
      // Process clipboard text
      #(model, effect.none())
    }
    _ -> #(model, effect.none())
  }
}
pub fn clipboard_write(
  text text: String,
  on_success on_success: msg,
  on_error on_error: msg,
) -> Effect(msg)

Write text to the system clipboard.

Must be called in response to user interaction due to browser security.

Example

type Msg {
  CopyScoreClicked
  CopiedToClipboard
  CopyFailed
}

fn update(model, msg, ctx) {
  case msg {
    CopyScoreClicked -> #(
      model,
      effect.clipboard_write(
        text: "High Score: " <> int.to_string(model.high_score),
        on_success: CopiedToClipboard,
        on_error: CopyFailed,
      ),
    )
    _ -> #(model, effect.none())
  }
}
pub fn delay(
  duration duration: duration.Duration,
  msg msg: msg,
) -> Effect(msg)

Delay dispatching a message by a specified duration.

Waits for a specific number of milliseconds using setTimeout.

Example

type Msg {
  PlayerHit
  ShowDamageEffect
  HideDamageEffect
}

fn update(model, msg, ctx) {
  case msg {
    PlayerHit -> #(
      Model(..model, health: model.health - 10),
      effect.batch([
        effect.from(fn(_) { show_damage_animation() }),
        effect.delay(500, HideDamageEffect),  // Hide after 500ms
      ]),
    )
    HideDamageEffect -> #(model, effect.none())
    _ -> #(model, effect.none())
  }
}
pub fn dispatch(msg msg: msg) -> Effect(msg)

Dispatch a message to be processed by the update function.

This is the core effect for game loops. Calling effect.dispatch(Tick) schedules a Tick message to be processed, which triggers another update cycle.

pub fn exit_fullscreen(
  on_success on_success: msg,
  on_error on_error: msg,
) -> Effect(msg)

Exit fullscreen mode.

Example

effect.exit_fullscreen(
  on_success: FullScreenExited,
  on_error: FullScreenExitedFailed
)
pub fn exit_pointer_lock() -> Effect(msg)

Exit pointer lock mode.

Example

effect.exit_pointer_lock()
pub fn from(effect: fn(fn(msg) -> Nil) -> Nil) -> Effect(msg)

Create a custom effect from a function.

The function receives a dispatch callback to send messages back to your update function.

Example

effect.from(fn(dispatch) {
  log("Player score: " <> int.to_string(score))
  dispatch(ScoreLogged)
})
pub fn from_promise(p: promise.Promise(msg)) -> Effect(msg)

Create an effect from a JavaScript Promise.

When the promise resolves, it dispatches the resulting message.

Example

let fetch_promise = fetch_data()
effect.from_promise(promise.map(fetch_promise, DataLoaded))
pub fn gamepad_vibrate(
  gamepad gamepad: Int,
  intensity intensity: Float,
  duration duration: duration.Duration,
) -> Effect(msg)

Trigger haptic feedback on a gamepad.

Intensity ranges from 0.0 (no vibration) to 1.0 (maximum vibration). Duration is in milliseconds.

Example

type Msg {
  ExplosionNearPlayer
}

fn update(model, msg, ctx) {
  case msg {
    ExplosionNearPlayer -> #(
      model,
      effect.gamepad_vibrate(
        gamepad: 0,
        intensity: 0.7,
        duration: duration.milliseconds(500),
      ),
    )
    _ -> #(model, effect.none())
  }
}
pub fn interval(
  duration duration: duration.Duration,
  msg msg: msg,
  on_created on_created: fn(global.TimerID) -> msg,
) -> Effect(msg)

Create a recurring interval that dispatches a message periodically.

Immediately dispatches on_created with the interval ID, which you should store in your model. Use this ID with cancel_interval to stop it later.

No global state - the interval ID is managed by JavaScript’s setInterval and you store it in your model.

Example

type Model {
  Model(spawn_interval: option.Option(timer.TimerId))
}

type Msg {
  StartSpawning
  IntervalCreated(timer.TimerId)
  SpawnEnemy
  StopSpawning
}

fn update(model, msg, ctx) {
  case msg {
    StartSpawning -> #(
      model,
      effect.interval(
        ms: 2000,
        msg: SpawnEnemy,
        on_created: IntervalCreated,
      ),
    )
    IntervalCreated(id) -> #(
      Model(..model, spawn_interval: option.Some(id)),
      effect.none(),
    )
    SpawnEnemy -> #(spawn_enemy(model), effect.none())
    StopSpawning ->
      case model.spawn_interval {
        option.Some(id) -> #(
          Model(..model, spawn_interval: option.None),
          effect.cancel_interval(id),
        )
        option.None -> #(model, effect.none())
      }
  }
}
pub fn map(effect: Effect(a), f: fn(a) -> b) -> Effect(b)

Map effect messages to a different type.

Useful when composing effects from subcomponents.

Example

let player_effect = player.update(player_model, player_msg)
effect.map(player_effect, PlayerMsg)
pub fn mobile_vibrate(
  pattern: List(duration.Duration),
) -> Effect(msg)

Trigger haptic feedback on mobile devices.

The pattern is a list of vibration durations in milliseconds. For example, [200, 100, 200] vibrates for 200ms, pauses 100ms, then vibrates 200ms.

Example

type Msg {
  PlayerHit
}

fn update(model, msg, ctx) {
  case msg {
    PlayerHit -> #(
      Model(..model, health: model.health - 10),
      effect.vibrate([100, 50, 100]),  // Quick buzz pattern
    )
    _ -> #(model, effect.none())
  }
}
pub fn none() -> Effect(msg)

Create an effect that performs no side effects.

Use when you want to update state without triggering any effects.

Example

fn update(model, msg, ctx) {
  case msg {
    Idle -> #(model, effect.none())
  }
}
pub fn request_fullscreen(
  on_success on_success: msg,
  on_error on_error: msg,
) -> Effect(msg)

Request fullscreen mode for the game canvas.

This must be called in response to a user interaction (click, key press, etc.) due to browser security restrictions.

Example

type Msg {
  FullscreenButtonClicked
  FullscreenEntered
  FullscreenFailed
}

fn update(model, msg, ctx) {
  case msg {
    FullscreenButtonClicked -> #(
      model,
      effect.request_fullscreen(
        on_success: FullscreenEntered,
        on_error: FullscreenEnteredFailed,
      ),
    )
    _ -> #(model, effect.none())
  }
}
pub fn request_pointer_lock(
  on_success on_success: msg,
  on_error on_error: msg,
) -> Effect(msg)

Request pointer lock for the game canvas.

This hides the cursor and provides unlimited mouse movement, commonly used in first-person games. Must be called in response to user interaction.

Example

type Msg {
  StartFPSMode
  PointerLocked
  PointerLockFailed
}

fn update(model, msg, ctx) {
  case msg {
    StartFPSMode -> #(
      model,
      effect.request_pointer_lock(
        on_success: PointerLocked,
        on_error: PointerLockFailed,
      ),
    )
    _ -> #(model, effect.none())
  }
}
Search Document