Spoiler
Because coroutines are one of the most advanced topics in Lua, I highlighted any important words in blue and defined them below. I have also done the same for important functions in red. You may want to peruse this list before we really get started, but it is most certainly not required and you may not understand everything listed here completely.
- Block - A chunk of Lua code; usually a function, control structure (such as loops), or variable definition
- Yielding Point - The point at which Lua steps out of the function and hands over control to its superior (whatever called coroutine.resume). There are two ways to yield in ComputerCraft:
- coroutine.yield([return args]) - The traditional way to yield. Used in "vanilla" Lua.
- os.pullEvent or os.pullEventRaw([event type]) - Not available to vanilla Lua, os.pullEvent behaves exactly the same as coroutine.yield because it essentially is coroutine.yield (see the spoiler below if you're especially curious about this). It will not get any events from a coroutine, and will only request that the parent pull an event. See later in the tutorial for more on this.
Spoiler
If you open up the bios from the ComputerCraft folder, you'll see this:
function os.pullEventRaw( _sFilter )
return coroutine.yield( _sFilter)
end
So in actuality, os.pullEvent is just a wrapper for coroutine.yield! Try it out in the lua console - if you use coroutine.yield, it is the same as using os.pullEventRawFunctions
- coroutine.create(function) –Called by the parent
- Creates a coroutine
- Returns a coroutine to be used at a later date
- coroutine.resume(coroutine, … [arguments]) –Called by the parent
- Resumes the specified coroutine and provides it with the given arguments, if there are any
- Returns a boolean which specifies a successful call to coroutine.resume
- Returns any additional arguments provided by the yield from the function
- coroutine.yield(return arguments) - Called from the function
- Yields to the parent function and provides it with any specified arguments
- Returns any values provided by coroutine.resume
- os.pullEvent([event to capture]) - This function is used exactly the same as coroutine.yield, because it is actually the same function. See above under terms for more information on this.
- coroutine.status - Gets the status of the coroutine
- Returns either "dead" or "suspended"
So you think you've got Lua down pat. Metatables? A breeze. HTTP? Pshh. You could handle that in your sleep.
But there's one thing you're missing - Multi-tasking. You're still using the parallel API, which although perfectly functional, is nevertheless an API. A dependency that you won't always be able to rely on if you're using regular Lua. Or maybe you just want to challenge yourself and design your own method of multi-tasking. No matter the reason, any proficient Lua programmer should be familiar with and knowledgeable in coroutines, which are the basis of all multi-tasking operations.
Spoiler
What are coroutines really? Although I said that they are a way of achieving multi-tasking in Lua, I was bending the truth slightly. In reality, coroutines allow you to jump in and out of lua blocks whenever you reach a yielding point. It does this at an extremely fast rate, creating the illusion of multi-tasking to us temporally barbaric homosapiens.Spoiler
Creating a coroutine is simple enough. All you have to do is call coroutine.create and provide it with a function as the first and only argument.
local function myFirstCoroutine()
print("Hi there, friend! I am a function running from a coroutine!")
end
local co = coroutine.create(myFirstCoroutine)
If you run the above code, nothing will happen. To use this coroutine, see the next section.
Spoiler
To run a coroutine, you must use the function coroutine.resume and provide it with the coroutine that we created in the first step.
coroutine.resume(co)
print(coroutine.status(co))
This will output: Hi there, friend! I am a function running from a coroutine!
coroutine.status is the status of the coroutine - in other words, does the function still have more code to run once you resume it again? If so, then this will output "suspended". Otherwise (such as in our example) it will be "dead". If the code is currently running, it will return "running", and if the coroutine has started another coroutine, it will return "normal"
Spoiler
coroutine.yield provides you with a way to stop a function mid-chunk and hand control back to the calling parent until coroutine.resume is used again. Let's take a look at a practical application of this:
local function runMe()
print("Hey part 1 is going.")
coroutine.yield()
print("Oh hey again. Now I'm in part 2!")
end
local co = coroutine.create(runMe)
coroutine.resume(co) --#in this case, the function is not done yet so a call to coroutine.status(co) will return "suspended"
--#Prints "Hey part 1 is going"
print("We are back in the parent code!")
coroutine.resume(co) --#now status is "dead" because the function has reached the end of its code
--#Prints "Oh hey again. Now I'm in part 2!")
That's pretty neat. We've stopped a function half way through, went back to the main code, and then returned to the function and finished out the remainder of the function.
Spoiler
If you read the definition of coroutine.resume at the beginning of this post, you would know that the function can actually take more than just the name of the coroutine as an argument: it can also take any other values that you would like to pass to the function as additional arguments as well. Let's look at an example:
local function giveMeValues(name)
print("Hi "..name)
end
local co = coroutine.create(giveMeValues)
coroutine.resume(co, "Bubba")
--#Outputs "Hi Bubba"
[size=5][size=4]
With coroutine.resume, you can provide as many arguments as you want to the coroutine in question.
Spoiler
In the above code, we passed an argument from a coroutine to the function as an initial value. But what if we want to get an argument from the parent caller in the middle of the function? Not to worry, coroutine.yield is there to save the day. It will return any arguments that are passed from a resume like in the following example:
local function middleArguments()
print("I'm at the beginning. Yielding now...")
local arg1, arg2 = coroutine.yield("Hey, this string is passed back to the caller")
print("Arg1: "..arg1)
print("Arg2: "..arg2)
end
local co = coroutine.create(middleArguments)
local resume_success, str_arg = coroutine.resume(co) --#Status is true
--#I'm at the beginning. Yielding now...
print(str_arg) --#Prints "Hey, this string is passed back to the caller"
status = coroutine.resume(co, "Argument 1!", "Argument 2!") --#Status is false
--#Arg1: Argument 1!
--#Arg2: Argument 2!
end
Now we can pass values to and from coroutines like nobody's business!
Spoiler
If you remember towards the top of this tutorial, I talked about yielding points. If you went up to the definition of yielding points, you may have noticed that os.pullEvent is defined as a yielding point (and if you looked more in depth, you will know that actually os.pullEvent is only a wrapper for coroutine.yield). If you didn't read those things, allow me to talk a little about how os.pullEvent works in coroutines.
local function eventPuller()
print("Hi")
local e = {os.pullEvent()}
print(unpack(e))
end
local co = coroutine.create(eventPuller)
local resume_status = coroutine.resume(co) --#Contrary to what one might think, resume_status will always be true, even if a coroutine is dead
print(coroutine.status(co))
In the above code, we create a coroutine that pulls events and resume it. Now you would expect it to do something like the following:
- Create the coroutine
- Resume it
- We are in the eventPuller function and print "Hi"
- We pull an event and print it, exiting the function
- We print the status, which you would think would be "dead"
- Create the coroutine
- Resume it
- We are in the eventPuller function and print "Hi"
- We print the status of the coroutine, which is actually "suspended"
local function eventPuller()
print("In event puller")
local arg = {coroutine.yield()}
print(unpack(arg))
end
Because os.pullEvent behaves this way, we'll have to get our events from outside of the coroutine and from the parent caller instead. Let's give it a try:
local function eventPuller()
print("In event puller")
local args = {coroutine.yield("char")} --#Let's request a char event
print(args[2])
end
local co = coroutine.create(eventPuller)
local _, event_type = coroutine.resume(co)
local event = {os.pullEvent(event_type)}
coroutine.resume(co, unpack(event))
This will do the following:
- Print "In event puller" and exit to the parent
- Pull a "char" event
- Go back to the coroutine and give it the events
- Print out the character that we pulled
Spoiler
We know how to create coroutines, and we know how to get values into and out of them. But how do we perform more than one thing at a time? Well to tell the truth, you can't in Lua. Everything must be done sequentially. But with coroutines, we'll jump into and out of functions fast enough that you won't be able to tell a difference between running at the same time and running sequentially.Let's look at an example:
local str = ""
local function saveString()
while true do
local e = {os.pullEvent("char")}
str = str..e[2]
end
end
local function writeString()
while true do
local e = {os.pullEvent("char")}
write(e[2])
end
end
local co1 = coroutine.create(saveString)
local co2 = coroutine.create(writeString)
local evt = {}
while true do
coroutine.resume(co1, unpack(evt))
coroutine.resume(co2, unpack(evt))
evt = {os.pullEvent("char")}
if evt[2] == " " then
break --#If there's a space, jump out of the loop
end
end
print("And the result of str is: "..str)
Type a word and then hit space. You'll see the word being written to the string as you type it, and once again once you hit the space bar.
Hopefully you've learned something from this tutorial. I know that coroutines can be a bit confusing, but after a bit of messing around with them they become more managable. I may add on to this tutorial at a later date, but right now I feel that I've written more than my schedule really allows for.
Thanks for reading,
- Bubba