What Is a Range?
In D, a range is not a specific type but a compile-time interface — any type that implements the right set of primitives is automatically a range, checked via templates like isInputRange!R. The simplest category, InputRange, requires three primitives: a bool empty property that reports whether iteration is finished, a front property that returns the current element without removing it, and a popFront() method that advances the range. Because this is structural typing (duck typing via templates), arrays, custom structs, and even lazy generators can all be passed to the same generic algorithms in std.algorithm without inheritance or explicit interface declarations.
Cricket analogy: A range is like a bowler's over card that only needs three facts — whether the over is complete (empty), which ball is coming up (front), and how to move to the next delivery (popFront) — regardless of whether it's Bumrah or a club bowler holding the ball.
Lazy, Composable Algorithms
Most of std.algorithm's building blocks — map, filter, take, until — do not eagerly allocate a new array; instead they return a lightweight struct that wraps the source range and computes each element only when front is evaluated. This means an expression like iota(1, 1_000_000).filter!(x => x % 7 == 0).map!(x => x * x).take(5) never touches most of the million numbers; only the first five results that satisfy the predicate are ever computed, because popFront on the outer range pulls values through the whole pipeline on demand. This laziness lets you build long algorithm pipelines cheaply and only pay for the work you actually consume, which is essential when working with infinite ranges like an unbounded Fibonacci generator.
Cricket analogy: It's like a highlights package generator that only renders a six or a wicket clip when the editor scrubs to that timestamp, rather than pre-rendering every ball of a Virat Kohli innings up front — you pay only for the highlights you actually watch.
import std.algorithm : map, filter;
import std.range : iota, take;
import std.stdio : writeln;
void main()
{
auto pipeline = iota(1, 1_000_000)
.filter!(x => x % 7 == 0)
.map!(x => x * x)
.take(5);
// Nothing has been computed yet -- pipeline is a lazy range.
foreach (value; pipeline)
writeln(value); // 49, 196, 441, 784, 1225
}std.range provides ready-made range adaptors — chain (concatenate ranges), zip (iterate several ranges in lockstep), retro (reverse a bidirectional range), and stride (skip every Nth element) — so you rarely need to hand-write a custom range for common patterns.
Custom Ranges and the Range Hierarchy
Writing your own range means implementing the primitives for the level of the hierarchy you need: InputRange (empty, front, popFront) is the baseline every foreach loop understands, ForwardRange additionally requires a save() method that returns an independent copy of the current iteration state so an algorithm can branch and retry without disturbing the original, and BidirectionalRange/RandomAccessRange add back()/popBack() and opIndex() respectively for reverse and indexed access. A crucial pitfall is that many custom ranges are reference types (classes or structs holding a pointer/slice), so passing one to a function or foreach that consumes it via popFront can silently exhaust the original range too — save() exists specifically so algorithms like std.algorithm.chunkBy or find can checkpoint progress safely.
Cricket analogy: It's like the difference between a bowler who can only bowl the next delivery (InputRange) versus one whose over can be "replayed" from a saved run-up via DRS review (save/ForwardRange) so the umpire can re-examine the same ball without losing track of the live over.
struct Countdown
{
int current;
bool empty() const => current < 0;
int front() const => current;
void popFront() => current--;
Countdown save() const => this; // makes it a ForwardRange
}
void main()
{
import std.stdio : writeln;
import std.algorithm : each;
auto c = Countdown(5);
c.each!writeln; // 5, 4, 3, 2, 1, 0
}InputRanges consumed by foreach or algorithms like find are mutated in place if they are reference types (classes, or structs wrapping a slice/pointer) — always call save() before branching, or you'll find the 'same' range has already been exhausted the second time you iterate it.
- A range is a compile-time interface, not a base class — any type with empty/front/popFront satisfies InputRange.
- The hierarchy (InputRange < ForwardRange < BidirectionalRange < RandomAccessRange) adds capabilities: save(), back()/popBack(), and opIndex().
- std.algorithm functions like map, filter, and take are lazy — they compute elements only when pulled via popFront.
- Laziness allows infinite or huge ranges (like iota over a billion values) to be processed cheaply.
- save() exists to let algorithms checkpoint a ForwardRange without disturbing the original iteration state.
- Reference-type ranges can be silently exhausted if consumed twice without calling save() first.
- std.range offers chain, zip, retro, and stride as ready-made adaptors instead of hand-rolled ranges.
Practice what you learned
1. Which three primitives must a type implement, at minimum, to satisfy D's InputRange interface?
2. What does calling save() on a ForwardRange accomplish?
3. Why does iota(1, 1_000_000).filter!(...).map!(...).take(5) only compute a handful of values?
4. What capability does BidirectionalRange add on top of ForwardRange?
5. What is a common pitfall when passing a reference-type custom range to two separate algorithms in sequence?
Was this page helpful?
You May Also Like
Compile-Time Function Execution (CTFE)
Understand how D executes ordinary functions at compile time to compute constants, validate data, and generate code before the program ever runs.
Error Handling in D
Learn D's split between recoverable Exceptions and unrecoverable Errors, the scope(exit)/failure/success guards, and std.exception helpers like enforce and nothrow.
Mixins and Metaprogramming
See how D's string mixins and template mixins let you generate and reuse code at compile time, powered by CTFE and compile-time reflection via __traits.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics