The purpose of this tutorial is to list some of these common bad practices and to provide better alternatives to them.
1. Globals
A global variable is a variable that is not declared local, like this:
x = 3
They are more complex than that, of course, but if you don't know what a global is, then don't worry. It's possible to write any program you desire without ever using globals, without any inconveniences. If you think you need a global, think harder, because you most definitely don't. Globals are stored in the global table, called _G, which is not bound to your program's execution. This means that any global variables you create will continue to exist after your program ends (and can be accessed from the Lua prompt, that's no good for storing passwords or similarly confidential information!), in addition to there being a performance penalty due to every global variable access being a table index (into _G).One notable exception is when you're writing APIs. Any functions that you want people to be able to use must be made global, or they will not be accessible. (Although I personally prefer a combination of returning a table from my libraries and loading them with dofile to avoid this.)
So make that:
local x = 3
x = 8 --# Note that reassigning a local variable looks like a global assignment, but isn't one.
Additionally, keep in mind that functions can be global too. This:
function f()
-- ...
end
should be this:
local function f()
-- ...
end
2. shell.run
There are perfectly valid reasons to use shell.run, however, too many people use shell.run to implement funcitonality in their program, such as shell.run("clear") or shell.run("pastebin", "get", "xxxxxx"). This is bad practice because it introduces the overhead of having to load an entirely separate program for petty things like clearing the screen. Additionally, OSes may not even provide these programs, leaving your program in the dark.
Instead of using shell.run("clear"), use term.clear(), instead of shell.run("bg worm") use shell.openTab("worm"). In general, find the function that does what you want in the ComputerCraft API instead of relying on other programs. If you can't find it, you can always look at the source code of these programs, since none of the CraftOS programs are magic; They can do just as much as you can.
3. sleep(0)
If you're using sleep(0), you're probably doing something wrong. People often add this line when they get a "Too long without yielding" error, when they should be using the event system. A common example is checking for changes in redstone state. Here's a flawed example program that outputs redstone to the right when there is redstone input on the left (like a repeater):
while true do
rs.setOutput("right", rs.getInput("left"))
sleep(0)
end
The infamous "Too long without yielding" error is created when a program has run too long without pulling an event (no other computer can run while yours does, until you pull an event). Adding sleep(0) works because it creates a 0.05 second timer, and pulls a timer event immediately after. This means we are checking the redstone input every 0.05s, which is completely unnecessary, since there is a redstone event that is triggered when the computer's redstone input changes, which is all we need:
while true do
rs.setOutput("right", rs.getInput("left"))
os.pullEvent("redstone")
end
Before you add sleep(0), check if there is an event for what you need first. You will in all likelihood find something that fits your needs. Using sleep(0) will make your computer execute as often as possible, putting extra load on the server, so make sure you avoid it as much as you can.4. Using the parallel API
A common question seen in chats and in Ask a Pro is "How do I pull multiple events at the same time?" or "How do I pull events while receiving redstone messages?". Similarly, a common reply is "Use parallel.waitForAll!". This is bad advice, however, since parallel is an ugly and overkill workaround for doing this sort of thing. Infact, just like with globals, if you think you need parallel, you're probably wrong. There are very few cases in which you need parallel, and in those cases, you'll probably end up writing your own coroutine manager anyway.
Here's a flawed example that prints mouse click coordinates, key presses and incoming rednet messages at the same time:
parallel.waitForAll(function()
while true do
local e, btn, x, y = os.pullEvent("mouse_click")
print(string.format("Clicked %d at %d, %d", btn, x, y))
end
end, function()
while true do
local id, msg, protocol = rednet.receive()
print(string.format("Got message \"%s\" from %d", msg, id))
end
end, function()
while true do
local e, char = os.pullEvent("char")
print(string.format("Pressed %s", char))
end
end)
This example uses parallel.waitForAll to run three event loops at once, when infact we can do this exact same thing with just one loop, which is much simpler and more efficient. Note that rednet.receive() is just os.pullEvent("rednet_message") with a timeout, meaning we can just pull that event. If we don't pass anything into os.pullEvent, it will pull any event it can find, meaning we just have to check which one it is by comparing its first return value (commonly called "e") with our desired event.
while true do
local e, p1, p2, p3, p4, p5 = os.pullEvent()
if e == "mouse_click" then
--# We're essentially renaming our variables here to fit the event better. Not necessary, it's just nicer code.
local btn, x, y = p1, p2, p3
print(string.format("Clicked %d at %d, %d", btn, x, y))
elseif e == "rednet_message" then
local id, msg, protocol = p1, p2, p3
print(string.format("Got message \"%s\" from %d", msg, id))
elseif e == "char" then
local char = p1
print(string.format("Pressed %s", char))
end
end