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
dispatch- Schedule a message to be processed (used for game loops)delay- Dispatch a message after a delayinterval- Dispatch a message repeatedly at fixed intervalsbatch- Combine multiple effects to run togethernone- No effect (for state-only updates)
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())
}
}