Coroutine Wrappers and Iterators
Coroutines: The One Feature That Makes Lua Special
Look, Lua has a lot going for it. Clean syntax, tiny footprint, embeddable in everything from game engines to IoT devices. But if there’s one feature that makes Lua genuinely special compared to other scripting languages, it’s coroutines.
Most languages these days make you jump through hoops for cooperative multitasking—async/await, promises, callbacks, the whole headache. Lua just… gives you coroutines. Built-in. No libraries, no fuss.
And yet, for some reason, coroutines confuse the hell out of people. I think it’s because the documentation jumps straight into “suspended”, “running”, “dead” states without explaining why you’d actually want this. So let me try a different approach: let’s talk about what you can actually do with them.
The Problem Coroutines Solve
Here’s the deal: regular functions in most languages are like water through a pipe—they flow in one direction and that’s it. You call them, they run, they return. End of story.
But sometimes you want to hit pause. Maybe you’re iterating over a massive tree structure and need to yield values one at a time. Maybe you’re generating an infinite sequence but don’t want to blow up your memory. Maybe you’re reading a huge file line by line.
This is where coroutines come in—they’re functions you can pause and resume. Think of it like a video game: you can save your progress (yield), go do something else, then load that save (resume) and pick up exactly where you left off.
In Lua, you create a coroutine with coroutine.create(), which takes a function. The coroutine starts paused (suspended), and you kick it off with coroutine.resume(). Simple enough, right?
A Better Way: Coroutine Wrappers
Now, here’s where most tutorials lose people. They show you coroutine.create() and coroutine.resume() and expect you to use that ugly boilerplate everywhere. No thanks.
A coroutine wrapper is just a helper function that hides all that mess. Instead of thinking about creating and resuming, you just… run your function. Shocking concept, I know.
-- A simple coroutine wrapper function
local function runCoroutine(fn)
local co = coroutine.create(fn)
local success, result = coroutine.resume(co)
if not success then
error("Coroutine error: " .. tostring(result))
end
return result
end
-- Example usage
local result = runCoroutine(function()
local sum = 0
for i = 1, 5 do
sum = sum + i
end
return sum
end)
print(result) -- Output: 15
Is this useful on its own? Not really—it’s just a convoluted way to call a function. But the pattern becomes powerful when you need more complex control. Add logging, error handling, the ability to pause mid-execution, resume later with new data… suddenly that wrapper is doing real work for you.
The Iterator Problem (And The Coroutine Solution)
If you’ve used Lua’s for loop, you’ve used iterators. They’re just functions that return successive values until they run out.
for i = 1, 10 do
print(i)
end
Behind the scenes, Lua calls that iterator function 10 times. The tricky part is writing iterators for complex data structures—like a binary tree, for instance. You need to track your position, remember where you are in the traversal, handle backtracking… it’s a mess.
Unless you use a coroutine.
-- Tree node constructor
local function tree(value, left, right)
return {
value = value,
left = left,
right = right
}
end
-- In-order traversal iterator using a coroutine
local function inorderTraversal(node)
return coroutine.wrap(function()
local function visit(n)
if n.left then
visit(n.left)
end
coroutine.yield(n.value)
if n.right then
visit(n.right)
end
end
if node then
visit(node)
end
end)
end
-- Build a sample tree
-- 4
-- / \
-- 2 6
-- / \ / \
-- 1 3 5 7
local root = tree(4,
tree(2, tree(1), tree(3)),
tree(6, tree(5), tree(7))
)
-- Use the iterator
for value in inorderTraversal(root) do
print(value)
end
-- Output:
-- 1
-- 2
-- 3
-- 4
-- 5
-- 6
-- 7
This is beautiful. The recursive traversal code looks exactly like what you’d write for synchronous execution—no state machine, no manual position tracking. The magic of coroutine.yield() handles pausing and returning control to the for loop.
The Two Roads: wrap() vs create()+resume()
Let me be honest: I used coroutine.wrap() for years without ever touching coroutine.create() directly. It’s just easier. But understanding both is important.
coroutine.create(fn) gives you raw power:
- The coroutine starts suspended until you explicitly resume it
- You get full control: resume with arguments, receive status, handle errors manually
- You can check
coroutine.status(co)to see if it’s “suspended”, “running”, or “dead” - Multiple resumes with custom arguments? No problem
coroutine.wrap(fn) gives you convenience:
- Returns a regular function you can call like any other
- Calling that function automatically resumes the coroutine
- Handles the resume internally—cleaner, simpler interface
- But you lose direct access to the coroutine object and status
Here’s the thing: for 90% of use cases (iterators, generators, simple async patterns), coroutine.wrap() is exactly what you want. Don’t overcomplicate it. Use create()+resume() when you actually need that granular control, not just because you saw it in a blog post.
local co = coroutine.create(function()
coroutine.yield()
end)
print(coroutine.status(co)) -- Output: suspended
coroutine.resume(co)
print(coroutine.status(co)) -- Output: dead
Generators: Because Infinite Lists Are Useful
A generator is just a function that produces a sequence of values on demand. Think of it like a factory that spits out one widget at a time, rather than building an entire warehouse full upfront.
Coroutines are perfect for generators because you can yield as many values as you want, whenever you want. No need to figure out how to “pause” a regular function—yield literally pauses it.
-- Number generator using coroutine.wrap
local function numberGenerator(start, increment)
return coroutine.wrap(function()
local current = start
while true do
coroutine.yield(current)
current = current + increment
end
end)
end
-- Generate even numbers starting from 0
local evens = numberGenerator(0, 2)
print(evens()) -- Output: 0
print(evens()) -- Output: 2
print(evens()) -- Output: 4
print(evens()) -- Output: 6
Infinite even numbers. 0, 2, 4, 6, forever. And yet—memory usage is basically zero. We’re not storing anything. Each call generates the next value on-demand and throws away the previous one.
This is the real magic of generators. You can model infinite sequences without going insane (or running out of RAM).
Chaining: Functional Pipeline Goodness
One thing I love about coroutine-based generators is how well they chain together. You can build up complex data transformations from simple, composable pieces:
-- Generator that yields numbers in a range
local function rangeGenerator(start, finish)
return coroutine.wrap(function()
for i = start, finish do
coroutine.yield(i)
end
end)
end
-- Filter function
local function filter(gen, predicate)
return coroutine.wrap(function()
for value in gen do
if predicate(value) then
coroutine.yield(value)
end
end
end)
end
-- Transform function
local function map(gen, transform)
return coroutine.wrap(function()
for value in gen do
coroutine.yield(transform(value))
end
end)
end
-- Chain: generate 1-10, filter odds, square results
local numbers = rangeGenerator(1, 10)
local odds = filter(numbers, function(n) return n % 2 == 1 end)
local squared = map(odds, function(n) return n * n end)
for n in squared do
print(n)
end
-- Output:
-- 1
-- 9
-- 25
-- 49
-- 81
This is the kind of code that makes you feel smart. Each function does one thing. They’re reusable. They compose. And best of all, it’s lazy—values flow through the chain one at a time, not as massive intermediate tables.
I promise you’ll thank yourself later when you need to debug why your pipeline is producing wrong values. Small, focused functions = easy debugging.
Real-World: Lazy File Reading
Here’s a pattern I actually use in production: processing large files without choking memory.
local function fileLines(filepath)
return coroutine.wrap(function()
local file = assert(io.open(filepath, "r"))
for line in file:lines() do
coroutine.yield(line)
end
file:close()
end)
end
-- Usage example (commented out since file may not exist)
-- for line in fileLines("data.txt") do
-- print(line)
-- end
Reading a 10GB log file? No problem. This’ll happily chew through it one line at a time, using roughly the same amount of memory regardless of file size. Compare that to io.read("*a") which would try to load the whole thing into memory and likely crash your script.
The Big Picture
Coroutine wrappers, iterators, generators—these aren’t just academic exercises. They’re the building blocks for:
- Game engines (update loops, animation systems)
- Network servers (handling multiple connections cooperatively)
- Data pipelines (lazy transformation of large datasets)
- parsers and lexers (tokenizing input streams)
Once it clicks—that moment when you “get” coroutines—you start seeing problems everywhere that fit this pattern. And that’s a good thing.
The best way to learn? Don’t just read this. Build something. An iterator for a linked list. A generator that reads from a network socket. A pipeline that processes CSV rows. Mess around. Break things. That’s how it sticks.
Go forth and yield.