Rogue Tasks

"Tasks" are a neat feature of Rogue. If you tag method with the [task] attribute it becomes an asynchronous method that can be used like a thread. It appears to be concurrent but it is actually executing as a series of updates on the single-threaded main update loop.

Two additional commands can be used inside task methods: 'yield', which pauses execution of the task until the next update, and 'await', which repeatedly yields until an operand task is finished and then evaluates to the return value of that task method.

Calling a task method gives you a "Task" object in return which you can 'await' or 'start'. start()ing a task is like starting a thread; it just starts going. Awaiting a task (only usable from another task, remember) blocks the original task until the second task is complete.

Here's a contrived example of an app initialization class that fetches a JSON list of image URLs from a server, downloads them one at a time, and then loads them as textures. A pre-existing event-based, task-based "HTTPGet" request class is assumed:

class FetchAssets
    asset_list = Asset[]

    method init

    method go_to_next_state
      # ...

    method fetch_assets [task]
      local list = await fetch_image_list
      if (list?)
        forEach (info in list)
          asset_list.add( Asset(info.url,info.filepath) )

        await download_each_asset
        await load_each_asset

      println "All resources loaded"

    method fetch_image_list->PropertyList
      local response = HTTPGet( "" )
      if (not response.success) return null
      return PropertyList( )

    method download_each_asset [task]
      forEach (asset in asset_list)
        await HTTPGet( asset.url, asset.filepath )

    method load_each_asset [task]
      forEach (asset in asset_list)
        asset.texture = Texture( asset.filepath )
        yield  # only load one per frame

The technical trick to task methods is that the commands in each task method are turned into an object. Local variables become properties and a big 'which' (switch) essentially keeps track of which command to execute next given whatever yields and awaits have happened.

So the compiler converts this task:

method print_task( first:Integer, last:Integer )->Integer [task]
  local x = first
  while (x <= last)
    println x
  return x

into something like this that's automatically hooked into the update loop:

method print_task( first:Integer, last:Integer )->Task

class Task_print_task
    ip     = 0
    first  : Integer
    last   : Integer
    x      : Integer
    result : Integer

    method init( first, last )

    method execute
      which (ip)
        case 1
          x = first
        case 2
          if (not (x <= last))
            ip = 3
            println x
        case 3
          result = x
          ip = -1  # signal "finished"