Dynamic Signal Function Composition in Functional Reactive Programming
Switches allow you to change the signal function being applied over time, enabling state transitions, mode changes, and dynamic behavior in FRP applications without breaking the pure functional paradigm.
In Yampa (an arrow-based FRP library), a switch is a combinator that allows you to dynamically change which signal function is currently active. This is essential for building real applications with multiple modes, states, or phases.
Think of switches as the FRP equivalent of state machines: they let you transition from one behavior to another based on events, while maintaining the compositional and pure nature of signal functions.
Without switches, you’d be stuck with a single, unchanging signal function for your entire program. Switches enable: game menus, mode transitions, dynamic object creation/destruction, and conditional behavior changes.
switch :: SF a (b, Event c) -> (c -> SF a b) -> SF a b
The most fundamental switching combinator. It runs the first SF until it produces an event, then uses the event’s value to generate a new SF to run from that point forward.
How it works:
SF a (b, Event c) continuouslyb values until an event firesEvent c fires with value c, call the continuation function (c -> SF a b)-- Example: Run a countdown, then switch to a different behavior
gameWithTimer :: SF () Render
gameWithTimer =
switch (countdownScreen >>> arr (\s -> (s, gameStart s))) $ \_ ->
mainGameplay
Yampa provides several switch variants that differ in timing (immediate vs delayed) and recurrence (one-time vs recurring).
The new SF processes the current input immediately when switching occurs.
switch, rSwitch, kSwitch
Use when the new SF should “see” the switching event’s input.
The new SF starts at the next time step. The current output is discarded.
dSwitch, drSwitch
Use when you need a clean boundary between behaviors.
Switch happens at most once, then the continuation runs forever.
switch, dSwitch
Can switch multiple times as events continue to fire.
rSwitch, drSwitch
dSwitch :: SF a (b, Event c) -> (c -> SF a b) -> SF a b
Like switch, but the new SF starts at the next time step. The current input that triggered the switch is not processed by the new SF.
rSwitch :: SF a b -> SF (a, Event (SF a b)) b
Starts with an initial SF, but can switch to new SFs repeatedly whenever events arrive in the input signal. Each event carries the new SF to switch to.
drSwitch :: SF a b -> SF (a, Event (SF a b)) b
Combines delayed observation with recurring behavior. The new SF starts at the next time step, and switches can happen multiple times.
kSwitch :: SF a b -> SF (a, b) (Event c) -> (SF a b -> c -> SF a b) -> SF a b
Advanced switch that gives the continuation function access to both the event value and the original SF, allowing you to preserve or inspect state across switches.
Yampa also provides parallel switches for managing collections of signal functions running concurrently.
pSwitch :: col (SF a b) -> SF (a, col b) (Event c) -> (col (SF a b) -> c -> SF a (col b)) -> SF a (col b)
Manages a collection of SFs running in parallel. When an event fires, all SFs can be replaced with new ones based on the event value.
Use pSwitch and variants (like dpSwitch) when managing multiple game objects, particles, or any collection of independent behaviors that might need to change together. This is the foundation of object routers in Yampa games.
dpSwitchrpSwitchpSwitchB - broadcasts same input to all SFsIf the new SF can also switch at time zero, and it refers back to the first SF, you can create an infinite loop of switches that never resolves. The continuation is evaluated strictly at switching time!
When a switch occurs, the new SF is applied immediately. If that new SF can also fire an event at time zero, a nested switch can happen. Be careful about composing switches that might trigger simultaneously.
program :: SF () Render
program =
switch menuScreen $ \case
StartGame -> switch gameplay $ \gameResult ->
gameOverScreen gameResult
Options -> optionsScreen
The Swont pattern wraps switches in a continuation monad for cleaner composition:
newtype Swont i o a = Swont { unSwont :: Cont (SF i o) a }
deriving (Functor, Applicative, Monad)
program :: SF () Render
program = foreverSwont $ do
menuChoice <- menuScreen
case menuChoice of
Start -> mainGame
Options -> optionsScreen
-- Managing multiple game objects that can be added/removed
objectManager :: SF (Input, Event [Object]) [Object]
objectManager =
dpSwitch initialObjects
detectCollisions
updateObjectCollection
You have a one-time state transition (title screen → game, game → game over).
You need repeated mode changes based on ongoing events (character states: idle → walk → jump → idle).
You need to inspect or preserve the current SF’s state when switching (pause/resume functionality).
Managing collections of objects that all need to be updated or replaced together (game entities, particles, bullets).
Switches are the mechanism for dynamic behavior in Yampa. They let you compose signal functions that change over time while maintaining purity and compositionality. The variety of switches (immediate/delayed, one-time/recurring, single/parallel) gives you fine-grained control over timing and state management.
Created with the Twilight Scholar design system · For more on Yampa, see the official documentation and Yampa Wiki