Verse Wiki — the Verse handbook for Blueprint authors
Chapter 6 · Lesson 19

Functions & Implicit Return: Packaging Combos into Skills

You have drawn function graphs in Blueprint: a few input pins, an output pin, a Return node at the end. This lesson translates that graph into a single Verse equation — parameters are the input pins, the return type is the output pin, and the last value the function body computes automatically becomes the return value, no Return node to drag in; along the way we'll see how functions get passed around as values, and how "extension functions" bolt new moves onto existing types. ⚔️

1. The Full Function Definition: One Equation, Five Parts

You've already called plenty of functions in earlier lessons: Print (Blueprint's Print String node) for logging, Sleep (Blueprint's Delay node) for counting seconds, Subscribe (binding a device event to your handler logic, like Bind Event) for hooking up callbacks… This lesson, it's your turn to build one. Verse has exactly one "full form" for function definitions — memorize it, and every variant you meet later is just its shadow:

skill_functions.verse
using { /UnrealEngine.com/Temporary/Diagnostics }

# Single-line form: signature = expression, done in one line
Add(A:int, B:int):int = A + B

# Multi-line form: newline after =, body indented 4 spaces
HealthAfterHit(Health:int, Damage:int):int =
    Remaining := Health - Damage
    Remaining

# When the caller needs no answer back, the return type is void
Cheer(Name:string):void =
    Print("Cheer for {Name}!")

Blueprint translation: each of these three snippets is a Blueprint function. Add is a function with two integer input pins (A, B) and one integer output pin, with a single Add node wiring A + B to the output. HealthAfterHit is the same, except it first computes Health - Damage into a temporary value Remaining, then wires that to the output. Cheer declares its return type as void, meaning it has no output pin and hands nothing back — it just fires a Print String.

Take Add(A:int, B:int):int = A + B: five parts, left to right. The function name (PascalCase by convention, same as variables); the parameter list, each parameter written as "name:type"; the return type after the colon, declaring what this skill hands over once the cast finishes; an =; and finally the function body. When the body does just one step of computation, it goes right after the =; a longer chain wraps to a new line and indents as a block — like the whole run of nodes wired downward from the entry point inside a Blueprint function graph, all belonging to that function.

Mind that =: to Verse, defining a function is the same act as creating a locked-down constant that never gets a Set node — binding a name to a thing. Add is not "a string of nodes that gets executed"; it is itself a value, and it can be picked up whole and passed along like a variable. Let's plant that seed here — it sprouts in section 4. 🌱

Those angle-bracket tags you've already met — <suspends>, <override> — always sit after the input parameters and before the output type, e.g. OnBegin<override>()<suspends>:void. They're called specifiers: <override> is the Override dropdown on a Blueprint function, and <suspends> means the body contains nodes with the little clock icon (Delay-style nodes that take a while). A dedicated lesson later this chapter takes them apart, so for now just learn their faces. One more warning: an ordinary function must have the = and a body; build only an empty shell (name, inputs, outputs, nothing wired inside) and hitting Compile lights up red.

2. Implicit Return: Whatever the Last Line Computes Is the Answer

In Blueprint, a function hands out its result by wiring a value into the Return node's output pin. Verse saves you the trip: the last value computed in the body automatically becomes the whole function's return value. Want to return something? Let it stand on the last line — as natural as writing the answer at the bottom of an exam sheet.

implicit_return.verse
# The last line is Total, so the function's value is Total
FinalScore(Kills:int, Assists:int):int =
    Total := Kills * 3 + Assists
    Total

# if/else is an expression too: whichever branch runs, its last expression is the answer
RankLabel(Score:int):string =
    if (Score >= 100):
        "Ace"
    else:
        "Rookie"

Blueprint translation: the top half, FinalScore, is a Blueprint function with two integer inputs and one integer output — it first computes Kills × 3 + Assists into Total, then wires Total to the output. In the bottom half, the if/else inside RankLabel is a Branch: a score of 100 or more takes one wire and the result is "Ace"; otherwise the other wire, and the result is "Rookie" — each wire connects its own string to the same string output.

Only two rules. First, as long as your function has an output (return type isn't void), the value the last line hands over must type-match the output pin: FinalScore promised an int, the final line's Total is an int — deal. Second, a return type of void means "this function has no output pin, caller, don't expect anything back", and then whatever ends the body doesn't matter.

The number-one trap of this lesson hides inside rule one: putting Print(...) (that Print String node) on the last line of the body. Print String hands out no value (its result is void), and a function that promised an int ending on it is a type mismatch — hit Compile and it goes red. The fix is simple — fire the debug line first, let the real answer close the show, or just reorder. Remember the mantra: the answer always sits in the last row.

Verse does have an explicit return (equivalent to actually dragging in a Return node in Blueprint and closing the wire out early), so you can hand in your paper ahead of time; but idiomatic Verse code almost never needs it — let the answer settle naturally onto the last line and the whole function reads like a mathematical equation. Every example in this tutorial uses this "last line is the answer" implicit return.

3. Parameters: Read-Only Admission Tickets

Parameters are the "admission tickets" the caller hands in — in Blueprint they're the function's input pins — and they have two quirks to learn. First, every ticket must be stamped with a type; the wrong type fails the check at the door. Second, parameters are read-only: you can't drag a Set node onto an input pin inside the function body to change it — the compiler snaps at you. Want to do arithmetic on top of a parameter? Create a variable var of your own in the variables panel, copy the value in, then Set that one instead.

params_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

params_device := class(creative_device):

    # Parameters are read-only: to change one, copy it into your own var slot first
    CountdownFrom(Start:int):void =
        # set Start -= 1    # Compile error: parameters are immutable
        var Current:int = Start
        set Current -= 1
        Print("{Current} seconds left")

    # ? prefix = named parameter; add a default value = optional parameter
    IncreaseScore(?Points:int = 1):void =
        Print("Adding {Points} points")

    OnBegin<override>()<suspends>:void =
        CountdownFrom(10)
        IncreaseScore()                 # Omitted, uses the default of 1
        IncreaseScore(?Points := 10)    # Passed by name; neither ? nor := can be dropped

Blueprint translation: params_device := class(creative_device) is a device Blueprint you place into the level. Inside it, CountdownFrom demonstrates the "parameters are read-only" rule we just covered — the commented-out line that tries to modify the parameter directly would error; the right move is creating the variable Current and dragging a Set. OnBegin is Event BeginPlay, the wire that fires automatically when the game starts — here it calls the countdown function and the scoring function in turn.

Now look at the question mark in IncreaseScore(?Points:int = 1): prefix a parameter name with ? and it becomes a named parameter — callers must spell it out by full name, like IncreaseScore(?Points := 10); pair it with a default value (just like the default-value box you fill in on an input pin in Blueprint) and it's also an optional parameter — leave it out and you get the default 1. The reverse doesn't work: an ordinary parameter without the ? can't be passed by name — Add(A := 1, B := 2) is an error. Only parameters wearing the ? get roll-call service.

Watching without practicing is a fake move. The reward machine below has two key pieces carved out: one is the return type that implicit return demands a match for, the other is the call syntax for a named parameter. Fill them back in and hit "Check Answers". ✍️

reward_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

reward_device := class(creative_device):

    # The last line is Coins * Bonus — the return type must match it
    Reward(Coins:int, Bonus:int):____ =
        Coins * Bonus

    Announce(?Times:int = 1):void =
        Print("Celebrating {Times} times!")

    OnBegin<override>()<suspends>:void =
        Total := Reward(10, 3)
        Print("Got {Total} coins")
        Announce(____ := 3)

4. Functions Are Values: Lending Skill Books and Bolting On New Moves

Time for the seed from section 1 to sprout: if defining a function is just "binding a name to a value", then of course a function can be picked up like an ordinary number and passed as a parameter to another function — lending your teammate the whole skill book instead of casting the spell for them. All the receiver has to do is declare that input in "function-signature shape" — spelling out what it eats and what it spits:

first_class_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

Double(N:int):int = N * 2
Triple(N:int):int = N * 3

# Parameter F is declared in signature shape, so it can catch any matching function
Apply(F(X:int):int, N:int):int = F(N)

# Extension function: bolt a new method onto the existing int type
(Score:int).Doubled():int = Score * 2

first_class_device := class(creative_device):

    OnBegin<override>()<suspends>:void =
        A := Apply(Double, 5)    # Passing Double itself — no parentheses
        B := Apply(Triple, 5)
        C := A.Doubled()         # Extension function — calls like a member method
        Print("A={A} B={B} C={C}")

Blueprint translation: Double and Triple are two ordinary Blueprint functions. Apply's first input F(X:int):int isn't an ordinary value but a "function socket" — any function that eats one int and spits one int plugs right in; inside the body, F(N) takes whatever got plugged in and runs it once. In OnBegin, Apply(Double, 5) plugs the Double "skill book" into the socket (note: no parentheses — with parentheses you'd be casting it on the spot). The last line, (Score:int).Doubled(), is an "extension function", covered in detail below.

Three things to notice. One: in Apply(Double, 5), Double has no parentheses — parentheses are the "cast skill" button; don't press it, and what gets handed over is the skill book itself. Flip that around and you get a classic trap: X := Foo (forgot the parentheses) doesn't actually run FooX gets the function itself, and you'll likely harvest a baffling type mismatch further down. Two, in passing: functions are passed by reference — Apply receives Double in person, not a copy. Three: Verse currently has no way to "whip up a nameless little function on the spot" (other languages call that a lambda), and a function can't bundle up the surrounding variables and take them along; if you want to pass something, pass a function with a proper name. To make a callback "carry data with it" (like wanting to stuff extra info in when you Bind Event in Blueprint), store the data in a class and write the callback as a method of that class — the full Handler pattern is in this lesson's extras.

The last new face in the code is the extension function (extension method): write the "receiver" in parentheses before the function name — (Score:int).Doubled():int = Score * 2 — and you've bolted a new move onto int; call it as A.Doubled(), exactly like a method the object was born with. It can attach to any type visible to you — engine types, other people's Blueprint classes, structures (struct), enumerations (enum) — no creating a Child Blueprint Class to inherit from. Inside the body, refer to that object via the receiver name you declared (here Score); note it hasn't actually grown into the class, so it can't touch the private internal state the type keeps hidden. The official style guide (think of it as the official wiring/writing conventions) even recommends extension functions in place of one-parameter utility functions — the reasoning and the tricks are in this lesson's extras.

5. Field Assembly: When Should You Split Out a Function?

Put on all this lesson's gear and assemble a combo damage calculator: single-line function, multi-line function, implicit return, functions calling functions — the whole package. Click "Run Next Step" and watch with your own eyes how a function "goes in, comes back with the answer". 👇

combo_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

combo_device := class(creative_device):

    # Single-line function: combo count times 5 is the base damage
    BaseDamage(Combo:int):int = Combo * 5

    # Multi-line body: compute the bonus, then seat the answer in the last row
    FinalDamage(Combo:int, Crit:logic):int =
        Base := BaseDamage(Combo)
        if (Crit?):
            Base * 2
        else:
            Base

    OnBegin<override>()<suspends>:void =
        Normal := FinalDamage(3, false)
        Lucky := FinalDamage(3, true)
        Print("Normal combo: {Normal} damage")
        Print("Crit combo: {Lucky} damage")
Output Log

Click "Run Next Step" to watch the code execute line by line.

So when should logic be split into a function? Four rules of thumb: The moment you're about to copy a chunk of code a second time, don't paste — extract a function; you want bug fixes to happen in exactly one place. When a chunk of code needs a comment to be understood, give it a good name instead — the name is the best comment. OnBegin (Event BeginPlay) should read like a table of contents, with the details living inside functions — the stepper's main flow above is the model. One function does one thing, and its verb of a name says what. Also, you can nest a small function inside another function's body — it's visible only inside that outer function and can't be called anywhere else, perfect for little helpers that are "only useful right here".

To close, here's the quick-reference table of this lesson's common pitfalls — step on each one once and you're immune:

Blueprint Cross-Reference

This lesson squeezed into one lookup table: on the left, what you do inside a function graph; on the right, its shape in code. The difference column is the one that matters.

In Blueprint In Verse Difference
My Blueprint panel → Functions → add a new function graph An equation written in the class body, BaseDamage(Combo:int):int = Combo * 5 A Blueprint function must live on some Blueprint; a Verse function can also sit at the top level of a file, belonging to no class at all — like Double and Triple
A function's input pins (Inputs) The parameter list in parentheses, (A:int, B:int) Pin order becomes argument order — you can no longer wire them in any order you like; to pass by name, a parameter must wear the ?
The default-value box on an input pin ?Points:int = 1 Blueprint lets any input pin carry a default; Verse allows defaults only on named parameters wearing the ?
A function's output pins (Outputs) — you can add several The return type after the colon, :int A Verse function hands over exactly one value; want several output pins, pack them into a tuple (Lesson 18) and hand that over
The Return Node, with values wired into its pins Implicit return: the body's last expression is the return value No node to drag in, and none to forget; the price is that "the answer must sit in the last row" — end on a Print and Compile goes red
A Pure node (tick Pure and the execution pins vanish) The effect specifier <computes> Blueprint's Pure only means "no execution pins", and the value gets re-evaluated on every use; <computes> is a Compile-enforced promise of no side effects — Lesson 22 goes deep
A Macro, which can have several execution outputs No direct counterpart Verse has no macros. To express "this route may not go through", use <decides> with square-bracket calls; to reuse a stretch of flow, just extract a function
A Call Function node dragged off an object pin A.Doubled() / Apply(Double, 5) Calls are written inline; and the function itself can be handed over without parentheses as a value — Blueprint has no way to pass a function graph along a data pin

The root of all these differences is one sentence: a Blueprint function is a graph; a Verse function is a value. A graph must live inside some Blueprint asset and talks to the outside world through pins and wires, so it can have any number of inputs and outputs — but it can never be "picked up" and handed to someone else. Verse demotes the function to the same rank as a number or a string — a name bound to a thing — so it can be passed as an argument and stored in a variable. The price is a narrower interface: one value out, and anything extra you pack yourself.

Implicit return is a second trade-off. Blueprint's Return node is explicit and visible; you can spot the exit on the canvas at a glance. Verse hides the exit inside the "last line" convention, so reading takes one extra beat — but the function reads like an equation in return. When you genuinely need to hand your paper in early, Verse does have an explicit return; idiomatic code just almost never reaches for it.

Level Challenge 🏁

Skill bar configured — clear three mini challenges to sign off. Zero penalty for wrong answers; retry as often as you like.

A function that promised to hand over an int has Print("Done computing") (i.e. a Print String) as the last line of its body. What happens?

Foo is a function. You write X := Foo (no parentheses after it). What does X get?

You want to declare a parameter that can be omitted, but must be passed by name when provided. Which is correct?

Further Reading

Tip · EXTRA

Extension Methods: Let the Editor's Autocomplete Do the Work

Why does the official style guide recommend Value.Clamp() over Clamp(Value)? The "how easy is it to find" philosophy behind a single dot.

Open the extra →

Deep Dive · EXTRA

Are Functions First-Class Citizens? Passing Functions in Verse

A classic forum Q&A: declare an input in the shape of a function signature and it catches functions — plus the real-world boundary of "no whipping up nameless little functions (lambdas) on the spot".

Open the extra →

Tip · EXTRA

Passing Extra Parameters to Event Subscriptions? The Handler Class Pattern

Subscribe (binding device events) only eats one fixed shape of function — so how do you make a callback carry extra data? The standard solution, cited thousands of times by the community.

Open the extra →