Reference Verse compiles

Compose Small Functions: Clean Verse on a Sunlit Dock

Big OnBegin blobs are the enemy of readable Verse. Composing small, single-purpose functions — each doing one thing — keeps your pirate-island logic easy to read, test, and reuse. In this article you'll wire a sunlit dock challenge together from tiny pieces: a trigger that starts a countdown, a timer that tracks the swim, and a HUD message that cheers the player home.

Updated Examples verified on the live UEFN compiler
Watch the Knottrigger_device in ~90 seconds.

Overview

Compose isn't a placed device — it's a Verse language pattern for gluing small functions into bigger ones. On an island, gameplay is full of little transformations: a player's raw score becomes a bonus, a bonus becomes a reward tier, a reward tier decides which door opens. Writing all of that inside one 60-line event handler is where bugs are born.

Instead you write each step as its own tiny function — Double, Add1, ClampToTier — and then wire them together with Compose. The game problem it solves: readable, reusable, testable logic. Reach for it whenever you catch yourself nesting F(G(H(x))) by hand, or copy-pasting the same math into three different buttons on your sunny boardwalk.

The core building blocks are:

  • Compose — takes two functions and returns a new function that runs them in sequence (Compose(F, G) applies F first, then G — or the reverse depending on how you define it; be explicit!).
  • Partial application — bakes one argument into a function to produce a smaller, more specialized function (Add5 := Partial(Add, 5)).
  • Effect subtyping — because <computes> functions compose cleanly, your tiny helpers stay pure and predictable.

We'll trigger all of this from real placed devices — a button_device on the dock and a conditional_button_device at the vault — so the composed math actually does something in-game.

API Reference

(API surface could not be resolved for this device.)

Walkthrough

Picture a sunny cove: a player smacks a Beach Score Button on the boardwalk. Each press feeds their tap-count through a chain of tiny functions to compute a reward score, and when the score crosses a threshold we play a celebratory VFX and grant an item. All the math lives in small composed functions — the event handler just calls the composed result.

# A cel-shaded beach-day scoring device built from composed small functions.
beach_score_device := class(creative_device):

    # The dock button the player taps to earn points.
    @editable
    ScoreButton : button_device = button_device{}

    # A visual/audio pop when the player hits the reward tier.
    @editable
    RewardVfx : vfx_spawner_device = vfx_spawner_device{}

    # How many times this player has tapped.
    var Taps : int = 0

    # --- tiny, pure building-block functions ---

    # Double the raw taps.
    Double(X : int)<computes> : int = X * 2

    # Add a flat sunny-day bonus.
    Add1(X : int)<computes> : int = X + 1

    # Compose two int->int functions: run F first, then G.
    Compose(F : type{_(:int)<computes>:int}, G : type{_(:int)<computes>:int})<computes> : type{_(:int)<computes>:int} =
        Local(X : int)<computes> : int = G(F(X))
        Local

    OnBegin<override>()<suspends> : void =
        # Wire the button press to our handler.
        ScoreButton.InteractedWithEvent.Subscribe(OnTapped)

    # Runs every time the player taps the beach button.
    OnTapped(Agent : agent) : void =
        set Taps += 1

        # Build the reward formula ONCE by composing small functions:
        # first Double, then Add1  ->  Score = Taps*2 + 1
        ScoreOf := Compose(Double, Add1)
        Score := ScoreOf(Taps)

        # When the composed score crosses the tier, celebrate.
        if (Score >= 11):
            RewardVfx.Enable()
            RewardVfx.Spawn()

Line by line:

  • @editable ScoreButton : button_device — the placed dock button; you assign the real device in the UEFN Details panel. Calling a device method requires it be an @editable field, never a bare variable.
  • @editable RewardVfx : vfx_spawner_device — the celebration effect at the cove.
  • Double and Add1 — two tiny <computes> functions. Each does exactly one thing, so each is trivial to test and reuse.
  • Compose(F, G) — takes two int->int functions and returns a new function Local that applies F then G. The return type type{_(:int)<computes>:int} describes "a function from int to int". Note it returns the function value Local, not a call to it.
  • OnBegin — subscribes OnTapped to the button's InteractedWithEvent. Subscriptions belong here.
  • OnTapped — increments taps, builds ScoreOf := Compose(Double, Add1) so ScoreOf(Taps) computes Taps*2 + 1, then enables and spawns the VFX once the tier is reached. The math reads like English because it's composed from named pieces.

Common patterns

1. Partial application to specialize a reward function

Bake in a fixed bonus so each button on the boardwalk grants a different amount without duplicating logic. Here a granted item pops when the partially-applied function clears a threshold.

# Uses Partial to pre-bake a bonus into a reusable scoring function.
beach_partial_device := class(creative_device):

    @editable
    BoardwalkButton : button_device = button_device{}

    @editable
    ItemGranter : item_granter_device = item_granter_device{}

    # Generic two-arg adder.
    Add(X : int, Y : int)<computes> : int = X + Y

    # Partial: capture the first argument, return a one-arg function.
    Partial(F : type{_(:int, :int)<computes>:int}, X : int)<computes> : type{_(:int)<computes>:int} =
        PartialFunc(Y : int)<computes> : int = F(X, Y)
        PartialFunc

    OnBegin<override>()<suspends> : void =
        BoardwalkButton.InteractedWithEvent.Subscribe(OnPressed)

    OnPressed(Agent : agent) : void =
        # Pre-bake a +5 sunny bonus into a specialized function.
        Add5 := Partial(Add, 5)
        Reward := Add5(3)  # 8
        if (Reward >= 8):
            ItemGranter.GrantItem(Agent)

2. Composing across three steps into one door-check

Chain three tiny functions so a vault door on the clifftop opens only when the composed value clears the gate. Here the composed function drives a conditional_button_device state.

# Chains three composed functions to decide whether the vault unlocks.
cliff_vault_device := class(creative_device):

    @editable
    VaultButton : conditional_button_device = conditional_button_device{}

    var Charge : int = 3

    Double(X : int)<computes> : int = X * 2
    Add1(X : int)<computes> : int = X + 1

    Compose(F : type{_(:int)<computes>:int}, G : type{_(:int)<computes>:int})<computes> : type{_(:int)<computes>:int} =
        Local(X : int)<computes> : int = G(F(X))
        Local

    OnBegin<override>()<suspends> : void =
        VaultButton.ActivatedEvent.Subscribe(OnActivated)

    OnActivated(Agent : agent) : void =
        # Compose Double then Add1, then feed it into itself for a 2-stage chain.
        Stage1 := Compose(Double, Add1)          # X*2 + 1
        Final := Compose(Stage1, Stage1)         # ((X*2+1)*2+1)
        Result := Final(Charge)                  # ((3*2+1)*2+1) = 15
        if (Result >= 15):
            VaultButton.SetShowInteractionTime(true)

3. Composing effect-consistent functions passed into iteration

Pure <computes> functions compose cleanly and can be handed to array logic — great for scoring several beach checkpoints at once.

# Applies a composed transform to each checkpoint score.
checkpoint_sum_device := class(creative_device):

    @editable
    TallyButton : button_device = button_device{}

    @editable
    ResultVfx : vfx_spawner_device = vfx_spawner_device{}

    Double(X : int)<computes> : int = X * 2
    Add1(X : int)<computes> : int = X + 1

    Compose(F : type{_(:int)<computes>:int}, G : type{_(:int)<computes>:int})<computes> : type{_(:int)<computes>:int} =
        Local(X : int)<computes> : int = G(F(X))
        Local

    OnBegin<override>()<suspends> : void =
        TallyButton.InteractedWithEvent.Subscribe(OnTally)

    OnTally(Agent : agent) : void =
        Transform := Compose(Double, Add1)
        Scores := array{1, 2, 3}
        var Total : int = 0
        for (S : Scores):
            set Total += Transform(S)   # (1*2+1)+(2*2+1)+(3*2+1) = 3+5+7 = 15
        if (Total >= 15):
            ResultVfx.Enable()
            ResultVfx.Spawn()

Gotchas

  • Order matters. Compose(F, G) in our examples runs F first, then G (G(F(X))). If you flip the argument order you flip the math. Always document which function runs first — a comment saves hours.
  • Return the function, not a call. Inside Compose, you define Local and then write Local on its own line to return the function value. Writing Local(X) would try to call it with no valid arg and fail to compile.
  • Effect specifiers must line up. Composing <computes> functions produces a <computes> function. If one helper had a stronger effect (like <transacts>), the composed type must reflect it. Keep your tiny helpers pure (<computes>) whenever possible so they compose freely.
  • No int↔float auto-convert. If one function returns int and the next expects float, composition won't type-check. Keep the chain in one number type, or add an explicit conversion function to the chain.
  • Devices still need @editable + subscription. Composition is pure math; nothing happens in-game until a real device event (InteractedWithEvent, ActivatedEvent) calls your composed function AND the handler calls a real device method like Enable(), Spawn(), or GrantItem().
  • Unwrap agents when the event provides one. Handlers subscribed to InteractedWithEvent receive an agent; use it directly (as above) or, for listenable(?agent) events, unwrap with if (A := Agent?): before use.

Guides & scripts that use trigger_device

Step-by-step tutorials that put this object to work.

Build your own lesson with trigger_device

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →