Skip to content

Generators and Infinite Sequences

A generator is declared with fn*. Each call to the instance returns the next yield; when the function ends, it returns nil. Because execution is suspended between yields, generators can be infinite without consuming memory:

Finite counter, infinite Fibonacci with loop/yield, and a parameterized range with step — all consumed manually.

09-generators-fnstar.zolo
Playground
// Feature: Generators (`fn*` + `yield`)

// Syntax: `fn* name(args) { yield value }`. Each call to the instance

// returns the next `yield`; once finished, returns `nil`.

// When to use: infinite sequences, custom lazy values, sources that do

// not fit in memory, simple state machines.


// Finite counter.

fn* counter(limit: int) {
  var i = 0
  while i < limit {
    yield i
    i += 1
  }
}

let gen = counter(3)
print(gen())  // 0

print(gen())  // 1

print(gen())  // 2

print(gen())  // nil


// Infinite generator — possible because it is lazy.

fn* fibonacci() {
  var a = 0
  var b = 1
  loop {
    yield a
    let temp = a + b
    a = b
    b = temp
  }
}

let fib = fibonacci()
for i in 0..7 {
  print("fib({i}) = {fib()}")
}

// expected:

//   fib(0) = 0

//   fib(1) = 1

//   fib(2) = 1

//   fib(3) = 2

//   fib(4) = 3

//   fib(5) = 5

//   fib(6) = 8


// Generator parameterized with a step.

fn* step_range(start: int, stop: int, step: int) {
  var i = start
  while i < stop {
    yield i
    i += step
  }
}

let evens = step_range(0, 10, 2)
var v = evens()
while v != nil {
  print("  even {v}")
  v = evens()
}

Zolo also supports literal infinite ranges (0..) and additional operators such as scan, windows, sum, and Iter::from_fn. The golden rule is to always pair a take(n) before any terminal consumer:

Even squares, sum with .sum(), Fibonacci via Iter::from_fn, sliding windows, scan, zip of infinites, and Iter::repeat_val.

11-lazy-and-infinite.zolo
Playground
use std::Iter

// Lazy infinite iterators — examples

//

// Key rule: infinite ranges (0..) must always be paired with

// a terminator like take() before collect() or each().


// 1. First 5 squares of even numbers starting from 0

let squares = 0..
  |> .map(|x| x * x)
  |> .filter(|x| x % 2 == 0)
  |> .take(5)
  |> .collect()

print(squares)

// [0, 4, 16, 36, 64]


// 2. Sum of first 10 natural numbers (0..9)

let total = 0..
  |> .take(10)
  |> .sum()

print(total)

// 45


// 3. Fibonacci sequence via Iter.from_fn

fn fib_iter() {
  var a = 0
  var b = 1
  return Iter::from_fn(|| {
    let val = a
    let next = a + b
    a = b
    b = next
    return val
  })
}

let fibs = fib_iter()
  |> .take(10)
  |> .collect()

print(fibs)

// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]


// 4. Sliding windows of size 3 over first 6 naturals

let windows = 0..
  |> .take(6)
  |> .windows(3)
  |> .collect()

print(windows)

// [[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5]]


// 5. Scan (running sum)

let running = 1..
  |> .take(5)
  |> .scan(0, |acc, x| acc + x)
  |> .collect()

print(running)

// [1, 3, 6, 10, 15]


// 6. Zip two infinite ranges

let pairs = Iter::zip(0.., 1..)
  |> .take(4)
  |> .collect()

print(pairs)

// [[0, 1], [1, 2], [2, 3], [3, 4]]


// 7. Infinite repeat

let ones = Iter::repeat_val(1)
  |> .take(5)
  |> .sum()

print(ones)

// 5


// 8. for loop with infinite range (use break to exit)

for i in 0.. {
  if i >= 5 { break }
  print(i)
}
// 0, 1, 2, 3, 4

Long pipelines that combine pipe, tap, and named functions translate complex transformations into readable step-by-step code:

String pipeline with tap; numeric data-first pipeline with map_arr/filter_arr/sum_arr; step-by-step inspection with variables.

10-long-pipelines.zolo
Playground
// Feature: Long pipelines — full composition

// Syntax: combines `|>`, `&.`, and utility functions.

// When to use: small ETL, collection transformations, normalization,

// any case where the pipeline tells the story of the data flowing.


fn shout(s: str) -> str {
  return s.to_upper()
}

fn add_bang(s: str) -> str {
  return s + "!"
}

fn pad_dashes(s: str) -> str {
  return "-- " + s + " --"
}

fn log_str(s: str) -> str {
  print("  [step] {s}")
  return s
}

// String pipeline with tap between steps — the classic shape.

let banner = "hello"
  |> shout() &. log_str()
  // HELLO

  |> add_bang() &. log_str()
  // HELLO!

  |> pad_dashes()
// -- HELLO! --

print(banner)

// expected:

//   [step] HELLO

//   [step] HELLO!

//   -- HELLO! --


// Long numeric pipeline: 1..10 -> *3 -> evens only -> sum.

fn triple(x: int) -> int {
  return x * 3
}

fn is_even(x: int) -> bool {
  return x % 2 == 0
}

fn map_arr(arr: [int], f: fn(int) -> int) -> [int] {
  var out: [int] = []
  for x in arr { out.push(f(x)) }
  return out
}

fn filter_arr(arr: [int], pred: fn(int) -> bool) -> [int] {
  var out: [int] = []
  for x in arr {
    if pred(x) { out.push(x) }
  }
  return out
}

fn sum_arr(arr: [int]) -> int {
  var total = 0
  for x in arr { total += x }
  return total
}

// Data-first pipeline without tap (chaining tap on arrays in sequence

// has a runtime gotcha — break out the intermediate step into a

// variable when you want to inspect arrays).

let result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  |> map_arr(triple)
  |> filter_arr(is_even)
  |> sum_arr()

print("total = {result}")

// expected: total = 90


// To inspect steps in an array pipeline, prefer variables and direct

// `print(arr)` calls (interpolating an array inside a string only

// shows the handle).

let tripled = map_arr([1, 2, 3, 4, 5], triple)
print("  [tripled]")
print(tripled)
let only_even = filter_arr(tripled, is_even)
print("  [even]")
print(only_even)
let total = sum_arr(only_even)
print("  [total] {total}")
// expected:

//   [tripled]

//   [3, 6, 9, 12, 15]

//   [even]

//   [6, 12]

//   [total] 18

Challenge

Write a generator fn* powers(base: int) that produces increasing powers of base (1, base, base², base³, …) and use it to print the first 5 powers of 2.

enespt-br