Verse Wiki — the Verse handbook for Blueprint authors
Deep Dive · EXTRA

Why range Is Not a First-Class Value

Try storing 0..10 in a variable — Compile goes red and refuses on the spot. Starting from a few restrictions in the official docs, this page talks through why a range is just a "one-shot counting notation" rather than a proper value you can store and pass around, and hands you replacements for everything the docs do not support directly: counting down, stepping, and more.

1. The Restrictions, in Black and White

The official Range in Verse page nails down exactly what ranges can do: they count integers only; add 1 each step; only go from small to large; and — here comes the headline — the 0..N notation can only appear in four places: for, sync, race, and rush (the last three are concurrency features covered in later lessons). In other words, all of the following, perfectly ordinary in other languages, are simply not a thing in Verse:

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

# Legal: the range appears directly inside a for
for (I := 0..10):
    Print("Round {I}")

# All of the following are compile errors — a range is not a value:
# MyRange := 0..10          # can't be stored in a variable
# TakeRange(0..10)          # can't be passed as an argument
# (0..10).Length            # can't have methods called on it

Node-for-node translation: only the first snippet is legal — a loop counting 0 to 10, printing "Round __" each time with Print String. The three lines below are commented out because Compile would flag every one of them red: a range cannot be stored in a variable, cannot be fed to another function as an argument, and cannot be asked for its length the way an array can. A range is not a "thing" — it is just the act of counting.

Which is to say, 0..10 is not "a range object" but a throwaway counting notation — no type, no identity, nothing you can point at and call "the value"; it only gets unrolled in place into "count from here to there" at those four blessed positions. Programming calls something that can be stored in a variable, passed as an argument, and returned from a function a "first-class value" — and Verse's range explicitly is not one.

2. Syntax Sugar vs First-Class Value: a Design Trade-off

Compare the neighbors: in other languages a range is often a bona fide object — storable, passable, sliceable, measurable, sometimes with a step size or a reverse gear. Verse took the other road: make the range a purely "one-shot counting notation", serving exactly one scenario — repeating a fixed number of times.

What does the deal buy? Simplicity. The language never has to build a dedicated "range type", define how it converts to and from arrays, or answer the chain of follow-up questions like "what about decimal ranges?" and "what would counting backwards mean?"; for and the three concurrency spots each unroll "count from M to N" directly, and that is the whole story. The cost is just as plain: no step size, no counting down, no compute-only-when-needed lazy sequences — want those, roll your own. You will meet this kind of trade-off — chopping off a whole class of frills in exchange for a leaner language — again and again in Verse; it is the same temperament as "changing a value must go through a Set node" and "pass-or-fail checks must stay in a context built to handle both outcomes".

3. The Workaround List: Countdowns, Stepping, and Empty Ranges

Restrictions or not, the work still has to get done. For the counting patterns the docs do not support directly, the standard replacements follow — the core idea is always "let the range count obediently from small to large, and hand the conversion to the loop body or an intermediate variable in the parentheses":

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

countdown_device := class(creative_device):

    N:int = 5

    OnBegin<override>()<suspends>:void =
        # Countdown: 10..0 runs zero rounds; the right move is ascending + N - I arithmetic
        for (I := 0..N):
            Countdown := N - I
            Print("Countdown: {Countdown}")

        # Step of 2: no step keyword — build the sequence with multiplication
        Evens := for (I := 0..5) { I * 2 }
        Print("Got {Evens.Length} even numbers")

        # Empty range 0..-1: no error, zero rounds — safe for empty arrays
        Empty:[]int = array{}
        for (I := 0..Empty.Length - 1):
            Print("This line never prints")

Item by item: the countdown uses N - I — as I climbs from 0 to N, Countdown falls from N to 0. Step-2 uses I * 2, and incidentally shows off Lesson 14's trick of for spitting out an array when it finishes. The last one is the rare tender side of ranges "not being proper values" — an empty range (start bigger than end) raises no error and no warning, the body runs zero rounds, so "0 .. array length - 1" is forever safe on an empty array. Flip it around and the tenderness has a dark side: when your hand slips and writes 10..0, Compile stays just as silent and the loop quietly vanishes — faced with the mystery of "why did my loop never run?", check which end of the range is bigger, first thing.

4. Pop Quiz

You want to print 10, 9, 8… all the way down to 0. Which of these works?

Sources

Compiled from the official Epic documentation: Range in Verse ↗, with reference to Control Flow in Verse ↗.