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:
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":
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 ↗.