Coroutines Examples

Hi,

by popular demand (ok, 1 person asked for it :wink: ), I have put together a few simple examples on how to work with luas coroutines. The first one is this:

local cr

local function f()
    local n
    for n = 1, HEIGHT do
        coroutine.yield(n)
    end
end

function setup()
    cr = coroutine.create(f)
end

function draw()
    local ok, y = coroutine.resume(cr)
    if ok then
        background(40, 40, 50)
        fill(255, 255, 255, 255)
        ellipse(WIDTH / 2, y, 30)
        text(y, 100, HEIGHT - 20)
    else
        close()
    end
end

This is a very basic setup of a main function and a coroutine, which illustrates a few things about coroutines. First, in order to use a coroutine, you first have to initialize it with coroutine.create. This does not yet start the coroutine, just creates a handle and some context for it. The coroutine is started and continued using coroutine.resume. The second thing this illustrates is that coroutine.yield works as a kind of return from the function, to which you can later come back again using coroutine.resume. Arguments to coroutine.yield are returned as the 2nd, 3rd and so on results from coroutine.resume. The first value returned is a status which tells you if you can call the coroutine again, if it is true, or not, i it is false. The third thing this example illustrates is that if you come back to a coroutine with resume, the function is in exactly the state you left it in.

A common use for this simple setup is creating iterators for for ā€¦ in ā€¦ do ā€¦ end statements. This is so comon, tha a special function was introduced to ease that kind of stuff, namely coroutie.wrap. Example follows:


local function enum(a, b)
    return coroutine.wrap(
        function()
            local n
            for n = a, b do
                coroutine.yield(n)
            end
        end
    )
end

function setup()
end

function draw()
    local i
    background(40, 40, 50)
    fill(255, 255, 255, 255)
    font("AmericanTypewriter")
    fontSize(32)

    for i in enum(12, 17) do
        text(i, WIDTH / 2, HEIGHT - 32 * i)
    end
end

Coroutine.wrap calls coroutine.create on its argument and then returns a function that, when called, resumes the coroutine and returns all but the first (status) value returned by coroutine.resume.

For details about these functions, have a look at http://www.lua.org/manual/5.1/manual.html#5.2

More examples follow in the next 2 posts.

Part 2:

This is a more complex example of what yo can do with coroutines:


local cr

local function f(tx, ty)
    local x, y = math.random(WIDTH), math.random(HEIGHT)
    
    while true do
        local dx = tx - x
        local dy = ty - y

        if math.abs(dx) > math.abs(dy) then
            dy = dy / math.abs(dx)
            dx = dx / math.abs(dx)
        else
            dx = dx / math.abs(dy)
            dy = dy / math.abs(dy)
        end
        x = x + dx
        y = y + dy

        tx, ty = coroutine.yield(x, y)
    end
end

function setup()
    cr = coroutine.create(f)
end

local tx, ty = math.floor(WIDTH / 2), math.floor(HEIGHT / 2)

function draw()
    background(40, 40, 50)
    strokeWidth(5)
    stroke(255, 255, 255, 255)

    local ok, x, y = coroutine.resume(cr, tx, ty)
    if ok then
        fill(0, 255, 14, 255)
        rectMode(CENTER)
        rect(tx, ty, 15, 15)
        fill(255, 0, 0, 255)
        ellipse(x, y, 30)
    end
end

function touched(t)
    tx = t.x
    ty = t.y
end

This mainly illustrates the communication between the main function and the coroutines, which works like this: as mentioned before, arguments passed to coroutine.yield are returned from coroutine resume, but this also works th other way round. On the first call to coroutine.resume on a freshly created coroutine the arguments to coroutine.resume are passed as arguments to the coroutine function, and for all subsequent calls, the arguments to coroutine.resume are returned from coroutine.yield. Here I use it to notify the little circle that is handled by the coroutine about the position of the last touch, and the circle handling coroutine returns the position of the circle to the main program, which then draws it.

3rd example follows.

This is a yet somewhat more complex example, having a bunch of coroutines and a very simple scheduler for them:


local WorldWidth, WorldHeight = 16, 16
local MaxGrass = 3
local MaxSheep = 10
local MaxFed = 5

local cw = math.floor(WIDTH / WorldWidth)
local ch = math.floor(HEIGHT / WorldHeight)
local world = {}

local function initWorld()
    local x, y
    for x = 1, WorldWidth do
        world[x] = {}
        for y = 1, WorldHeight do
            world[x][y] = math.random(MaxGrass + 1) - 1
        end
    end
end

local function drawWorld()
    local x, y
    background(131, 90, 29, 255)
    pushStyle()
    strokeWidth(0)
    rectMode(CORNER)
    for x = 1, WorldWidth do
        for y = 1, WorldHeight do
            if math.floor(world[x][y]) > 0 then
                fill(0, 50 * world[x][y] + 100, 0, 255)
                rect((x - 1) * cw, (y - 1) * ch, cw, ch)
            end
        end
    end
    popStyle()
end

local function updateWorld()
    local x, y
    for x = 1, WorldWidth do
        for y = 1, WorldHeight do
            world[x][y] = world[x][y] + 0.05
            if world[x][y] > MaxGrass then world[x][y] = MaxGrass end
        end
    end
end

local function drawSheep(x, y)
    strokeWidth(3)
    stroke(0, 0, 0, 255)
    fill(255, 255, 255, 255)
    ellipse((x - 0.5) * cw, (y - 0.5) * ch, ch)
end

local function makeSheep(x, y)
    local fed = MaxFed
    local asheep = function()
        while fed > 0 do
            if math.floor(world[x][y]) > 0 then
                fed = fed + 1
                if fed > MaxFed then fed = MaxFed end
                world[x][y] = world[x][y] - 1
            else
                fed = fed - 1
                local n = math.random(4)
                if n == 1 then
                    x = x + 1
                elseif n == 2 then
                    y = y + 1
                elseif n == 3 then
                    x = x - 1
                else
                    y = y - 1
                end
                if x < 1 then x = 1 elseif x > WorldWidth then x = WorldWidth end
                if y < 1 then y = 1 elseif y > WorldWidth then y = WorldWidth end
            end
            drawSheep(x, y)
            coroutine.yield()
        end
    end

    return coroutine.create(asheep)
end

local sheep = {}

function setup()
    local n
    initWorld()
    for n = 1, MaxSheep do
        sheep[n] = makeSheep(math.random(WorldWidth), math.random(WorldHeight))
    end
end

local nsheep = MaxSheep
function draw()
    local dead = {}
    local n
    drawWorld()
    
    if nsheep > 0 then
        for n = 1, nsheep do
            local ok, err =  coroutine.resume(sheep[n])
            if not ok then
                -- print(err)
                dead[#dead + 1] = n
            end
        end
        
        for n = 1, #dead do
            print("sheep died")
            table.remove(sheep, dead[n])
            nsheep = nsheep - 1
        end
        
        updateWorld()
    else
        font("AcademyEngravedLetPlain")
        fontSize(82)
        fill(0, 0, 0, 255)
        text("all sheep died", WIDTH / 2, HEIGHT / 2)
    end
end

Here the ā€œsheepā€ are coroutines that keep their state within themselves instead of in an object. We also let the coroutine die if a sheep dies so that the main function knows about it. As noted before, coroutine.resume returns false as the first return calue, if the coroutine can not be resumed. This can basically mean one of 2 things: it has terminated, or it has thrown an error. In order to distinguish between these 2, you can check the 2nd return value, which will be the error message.

I hope this gives some insight about how to use coroutines. There are more advances uses like control reversal or helper threads, but these are beyond the scope of these simple examples. This should however get you started. If there are questions left, just ask away.

Thatā€™s brilliant, @gunnar_z. I havenā€™t actually used Luaā€™s coroutines that much so this was a really interesting read. It also reminds me that I may have forgotten to include them in the autocomplete system.

I have heard about coroutines, but frankly speaking I have no idea why to use those nor that I have understood the concept. Does this allow parallel processing of tasks, does it increase speed? So in a nutshell: why would I like to use coroutines and for what are they good for? Sorry for this stupid question, but I simply canā€™t get the point.

@CrazyEd coroutines are like cooperatively scheduled threads. They donā€™t increase speed because unlike preemptively scheduled threads they can not use multiple processors. They are a means to structure your program in such a way that you have independent parts loosely cooperating. Study the above examples, they can give you an idea about what coroutines can do for you.

Thanks so much! Wonderful!

Excellent explanation, @gunnar_z! @Simeon was impressed enough that when I was asking him elsewhere about coroutines, he pointed me here.

One of the reasons Iā€™m looking into them is that theyā€™ve been offered up as a ā€œbetter way to do callbacksā€, and Iā€™m trying to figure out just how that might happen. I still donā€™t understand it, yet - But I have never let that stop me before, and I wonā€™t let it do so now!

(one of the features in the upcoming beta that I hope passes muster with apple uses callbacks, and itā€™s less pretty than I had hoped it might be - Iā€™m trying to figure out how to wrap it up with coroutines so it looks more ā€œnormalā€ in usage, if that makes any sense).

@Bortels when I rework this for the wiki, I plan to also cover how to use coroutines for control reversal, maybe that will help with what youā€™re trying to do.

This is a really useful post thanks.

What Iā€™ll be using it for is the ā€œinitā€ phase of my app.

I intend to create as many of my resources as possible in code (I canā€™t draw for toffee) by drawing to offscreen images. This will allow me to just keep running the generation code in the background on the init screen and then allow a nice progress bar to be displayed (like the cargobot one) in the forergound which is simply scaled by a progress variable thatā€™s updated as resources are generated.