Skip to content
Joey Wang
Menu

Search

Full Stack Development

Closures, Blocks, and Iterators Across Six Languages

A working comparison of how Python, JavaScript, Ruby, PHP, Rust, and Go implement closures and lazy iteration, with a runnable counter and generator example for each.

· 10 min read

engineering #ruby#javascript#python#rust#go#php

Audio summary

Every language needs some way to carry state across calls without polluting global scope, and some way to produce a sequence of values without building the whole thing in memory first. Closures solve the first problem, iterators and generators solve the second. Python, JavaScript, Ruby, PHP, Rust, and Go take genuinely different approaches to both, shaped by what each language optimizes for: convenience, safety, or raw throughput. The best way to see the differences is the same example six times: a counter closure and a Fibonacci generator.


Python: closures and generators

Python closures capture variables from an enclosing scope. Mutating one of those variables from inside the closure requires declaring it nonlocal; without that, an assignment creates a new local variable instead of touching the outer one.

def create_counter():
    count = 0  # This variable is in the enclosing scope

    def increment():
        nonlocal count # Essential to modify 'count' from the enclosing scope
        count += 1
        return count
    return increment # Return the inner function

# Create separate counter instances
counter1 = create_counter()
counter2 = create_counter()

print(f"Counter 1 first call: {counter1()}") # Output: Counter 1 first call: 1
print(f"Counter 1 second call: {counter1()}") # Output: Counter 1 second call: 2
print(f"Counter 2 first call: {counter2()}") # Output: Counter 2 first call: 1

Each call to create_counter gets its own count. That’s the whole point: state that outlives the function call that created it, without touching a module-level global.

Generators solve a different problem: producing values lazily. A function containing yield returns an iterator that pauses at each yield and resumes exactly where it left off, keeping its local state intact between calls.

def fibonacci_generator(limit):
    a, b = 0, 1
    count = 0
    print(f"Generator initialized with limit: {limit}")
    while count < limit:
        yield a
        a, b = b, a + b
        count += 1
    print("Fibonacci sequence finished!")

# Create a generator instance
fib_gen = fibonacci_generator(5)

print(f"First value: {next(fib_gen)}")  # Output: Generator initialized with limit: 5 \n First value: 0
print(f"Second value: {next(fib_gen)}") # Output: Second value: 1
print(f"Third value: {next(fib_gen)}")  # Output: Third value: 1

# Using next() with a default value
value_if_found = next(('found' for k in [1, 2, 3] if k == 11), None)
print(f"Value if found: {value_if_found}") # Output: Value if found: None

This matters for anything that doesn’t need every value at once: streaming a large file, walking an infinite sequence, or chaining transformations without materializing intermediate lists.


JavaScript: closures and generators

JavaScript closures work the same way conceptually: an inner function keeps a live reference to variables in its enclosing scope even after the outer function has returned. It’s the mechanism behind most callbacks, event handlers, and module patterns in the language.

function createCounter() {
    let count = 0; // This variable is in the enclosing scope

    return function() { // This is the inner function, forming a closure
        count++;
        return count;
    };
}

// Create separate counter instances
const counter1 = createCounter();
const counter2 = createCounter();

console.log(`Counter 1 first call: ${counter1()}`); // Output: Counter 1 first call: 1
console.log(`Counter 1 second call: ${counter1()}`); // Output: Counter 1 second call: 2
console.log(`Counter 2 first call: ${counter2()}`); // Output: Counter 2 first call: 1

The state is invisible to anything outside the closure, which is as close as JavaScript gets to private instance variables without a class.

ES6 generators (function* and yield) give JavaScript the same pause-and-resume model as Python’s, and they’re the mechanism async/await is built on under the hood.

function* fibonacciGenerator(limit) {
    let a = 0, b = 1;
    let count = 0;
    console.log(`JS Generator initialized with limit: ${limit}`);
    while (count < limit) {
        yield a;
        [a, b] = [b, a + b]; // Array destructuring for swap
        count++;
    }
    console.log("JS Fibonacci sequence finished!");
}

const jsFibGen = fibonacciGenerator(5);

console.log(`JS First value: ${jsFibGen.next().value}`); // Output: JS Generator initialized with limit: 5 \n JS First value: 0
console.log(`JS Second value: ${jsFibGen.next().value}`); // Output: JS Second value: 1
console.log(`JS Third value: ${jsFibGen.next().value}`);  // Output: JS Third value: 1

Anything implementing the iterable protocol, generators included, works directly in a for...of loop.


Ruby: blocks, procs, and lambdas

Ruby blocks are the odd one out on this list: instead of being closures you construct and return, they’re anonymous chunks of code passed directly into a method call, with lexical access to variables in the scope where they were written.

def apply_action_to_numbers(numbers)
  numbers.each do |num|
    yield num # Executes the block passed to this method
  end
end

my_var = 10 # Variable in the outer scope

apply_action_to_numbers([1, 2, 3]) do |n|
  puts "Current number: #{n}"
  # Blocks can directly access and modify outer scope variables
  my_var += n
end

puts "Final my_var value: #{my_var}"
# Output:
# Current number: 1
# Current number: 2
# Current number: 3
# Final my_var value: 16 (10 + 1 + 2 + 3)

Blocks read and write the caller’s local variables directly, no nonlocal-style declaration needed. That’s what makes File.open(path) { |f| ... } work: the method controls setup and teardown, the block supplies the logic in between.

When you need to store a block as a value and pass it around rather than yield to it once, that’s what Proc and Lambda are for. Lambdas differ from plain procs in two ways: they check argument count strictly, and return inside a lambda returns from the lambda, not from the enclosing method.

def create_ruby_counter()
  count = 0 # Variable in the outer scope
  # Return a Proc object (a block converted to an object)
  Proc.new do
    count += 1
    count
  end
end

counter_a = create_ruby_counter()
counter_b = create_ruby_counter()

puts "Counter A first call: #{counter_a.call}" # Output: Counter A first call: 1
puts "Counter A second call: #{counter_a.call}" # Output: Counter A second call: 2
puts "Counter B first call: #{counter_b.call}" # Output: Counter B first call: 1

PHP: closures and generators

PHP closures need the use keyword to pull outer variables into scope, and by default that’s a copy taken when the closure is defined. Prefix the variable with & in the use clause to capture by reference instead, which is what lets the counter below actually increment across calls.

<?php
function createCounter() {
    $count = 0; // Variable in the enclosing scope

    return function () use (&$count) { // Use '&' for reference to modify outer variable
        $count++;
        return $count;
    };
}

$counter1 = createCounter();
$counter2 = createCounter();

echo "Counter 1 first call: " . $counter1() . "\n"; // Output: Counter 1 first call: 1
echo "Counter 1 second call: " . $counter1() . "\n"; // Output: Counter 1 second call: 2
echo "Counter 2 first call: " . $counter2() . "\n"; // Output: Counter 2 first call: 1
?>

PHP’s generators, like Python’s, use yield to produce values without holding the whole sequence in memory, which matters for anything reading a large file or a big database result set line by line.

<?php
function read_large_file($file_path) {
    if (!$file_handle = fopen($file_path, 'r')) {
        return; // Or throw an exception
    }
    while (!feof($file_handle)) {
        yield trim(fgets($file_handle)); // Yield one line at a time
    }
    fclose($file_handle);
}

// Process line by line without loading the entire file into memory
foreach (read_large_file('large_data.txt') as $line) {
    if (!empty($line)) {
        // Do heavy processing here
        break; // Process only the first line for example
    }
}
?>

Rust: ownership-aware closures and zero-cost iterators

Rust closures capture their environment according to how they use it: by reference, by mutable reference, or by taking ownership with move. The compiler infers which one applies based on what’s inside the closure body, and the Fn / FnMut / FnOnce traits describe the difference to anything that accepts a closure as a parameter.

fn create_incrementer() -> impl FnMut() -> i32 {
    let mut count = 0; // Outer variable

    // FnMut means the closure can mutate its captured environment
    move || { // 'move' keyword ensures 'count' is moved into the closure
        count += 1;
        count
    }
}

let mut counter1 = create_incrementer();
let mut counter2 = create_incrementer();

println!("Counter 1 first call: {}", counter1()); // Output: Counter 1 first call: 1
println!("Counter 1 second call: {}", counter1()); // Output: Counter 1 second call: 2
println!("Counter 2 first call: {}", counter2()); // Output: Counter 2 first call: 1

Rust has no yield keyword. Instead you implement the Iterator trait directly, with a next method returning Some(value) or None. The payoff for the extra ceremony is that the compiler optimizes the whole chain, so .map().filter().take() often compiles down to the same code as a hand-written loop.

struct Fibonacci {
    current: u32,
    next: u32,
}

impl Iterator for Fibonacci {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        let current = self.current;
        self.current = self.next;
        self.next = current + self.next;
        // Limit to prevent overflow for demonstration
        if current > 1000 {
            None // Stop iteration
        } else {
            Some(current)
        }
    }
}

// Function to create a new Fibonacci iterator
fn fibonacci() -> Fibonacci {
    Fibonacci { current: 0, next: 1 }
}

let mut fib_iter = fibonacci();
println!("Rust First value: {:?}", fib_iter.next()); // Output: Rust First value: Some(0)
println!("Rust Second value: {:?}", fib_iter.next()); // Output: Rust Second value: Some(1)
println!("Rust Third value: {:?}", fib_iter.next());  // Output: Rust Third value: Some(1)

// Using a for loop to consume the iterator
for num in fibonacci().take(10) { // take(10) limits to 10 items
    print!("{} ", num); // Output: 0 1 1 2 3 5 8 13 21 34
}
println!();

Go: closures and concurrency instead of generators

Go closures work like JavaScript’s: a function literal captures variables from its surrounding scope by reference.

package main

import "fmt"

func createCounter() func() int {
	count := 0 // Variable in the enclosing scope

	return func() int { // This anonymous function is the closure
		count++
		return count
	}
}

func main() {
	counter1 := createCounter()
	counter2 := createCounter()

	fmt.Printf("Counter 1 first call: %d\n", counter1())  // Output: Counter 1 first call: 1
	fmt.Printf("Counter 1 second call: %d\n", counter1()) // Output: Counter 1 second call: 2
	fmt.Printf("Counter 2 first call: %d\n", counter2())  // Output: Counter 2 first call: 1
}

Go has no yield either, but it doesn’t need one to get lazy sequences: goroutines and channels do the job directly. The generator runs as its own goroutine and blocks on a channel send until the consumer is ready for the next value.

package main

import "fmt"
import "sync" // For sync.WaitGroup to wait for the goroutine to finish

// fibonacciGenerator sends Fibonacci numbers to a channel
func fibonacciGenerator(limit int) (<-chan int, *sync.WaitGroup) {
	ch := make(chan int) // Channel for sending numbers
	var wg sync.WaitGroup
	wg.Add(1) // Add one goroutine to wait for

	go func() { // Start a new goroutine
		defer wg.Done() // Signal that this goroutine is done when it exits
		defer close(ch) // Close the channel when done to signal no more values

		a, b := 0, 1
		count := 0
		fmt.Printf("Go Generator (Goroutine) initialized with limit: %d\n", limit)
		for count < limit {
			ch <- a // Send 'a' to the channel (pauses if receiver not ready)
			a, b = b, a+b
			count++
		}
		fmt.Println("Go Fibonacci sequence finished!")
	}()
	return ch, &wg // Return the receive-only channel and the WaitGroup
}

func main() {
	fibChan, wg := fibonacciGenerator(5)

	fmt.Printf("Go First value: %d\n", <-fibChan)  // Output: Go Generator... \n Go First value: 0
	fmt.Printf("Go Second value: %d\n", <-fibChan) // Output: Go Second value: 1
	fmt.Printf("Go Third value: %d\n", <-fibChan)   // Output: Go Third value: 1

	// Continue consuming until the channel is closed
	for num := range fibChan {
		fmt.Printf("Go Next value: %d\n", num)
	}
	wg.Wait() // Wait for the generator goroutine to complete
}

The pattern generalizes to any producer/consumer relationship, not just sequences, which is why it’s idiomatic Go rather than a workaround for a missing feature.


Side by side

FeaturePythonJavaScriptRubyPHPRustGo
Core conceptFunctions remembering outer scope; pausable generatorsFunctions remembering lexical scope; pausable generatorsAnonymous code passed to methods; access to defining scopeAnonymous functions with use; pausable generatorsClosures that borrow/move; trait-based iteratorsClosures; concurrency primitives standing in for iterators
Mutating outer varsnonlocal requiredDirectly mutableDirectly mutable by defaultuse (&$var) for referencemove, or a mutable borrow (&mut)Directly mutable (captured by reference)
Lazy sequence mechanismyieldyield (function*)yield inside the yielding method, or EnumeratoryieldIterator trait, next returns Option<Item>Goroutine sending to a channel
Closure syntaxdef outer(): def inner(): nonlocal varfunction outer() { return function() {...} }Proc.new { ... } / lambda { ... }function() use ($var) { ... }move || { ... } or |var| { ... }func() { ... }

None of this is really about which language wins. Python and JavaScript optimize for readability, letting yield and closures fall out of ordinary syntax. Rust makes you spell out capture and ownership because it refuses to leave those decisions to a garbage collector. Go skips generators as a concept entirely and reaches for concurrency instead, because concurrency is supposed to be the easy path in Go, not the advanced one. The mechanism that trips people up most when moving between these languages is Ruby’s blocks: they look like closures, and mostly behave like them, but they aren’t first-class values until you convert them into a Proc.