Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Coroutines Examples

edited April 2012 in Examples Posts: 146

Hi,

by popular demand (ok, 1 person asked for it ;) ), 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.

Comments

  • edited April 2012 Posts: 146

    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.

  • Posts: 146

    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.

  • SimeonSimeon Admin Mod
    Posts: 4,837

    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.

  • Posts: 88

    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.

  • Posts: 146

    @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.

  • Posts: 2,820

    Thanks so much! Wonderful!

  • BortelsBortels Mod
    edited April 2012 Posts: 1,557

    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).

  • edited April 2012 Posts: 146

    @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.

  • Posts: 688

    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.

Sign In or Register to comment.