This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Lignum's profile picture

Efficiently working with multiple source files

Started by Lignum, 16 October 2016 - 06:59 PM
Lignum #1
Posted 16 October 2016 - 08:59 PM
It is often a good idea to split your program up into multiple files when it gets too large. Sadly, ComputerCraft does not provide any good ways to accomplish this. This tutorial shows one way to organise your source code.

While there are many different ways to do this, this method has the following advantages:
  • It does not touch the global table, making access to external variables fast, while also keeping _G unpolluted.
  • Each source file will only be executed once, meaning that, just like os.loadAPI, everything defined in the file will only be defined once.
  • Less importantly, source files can return any type of data, not just tables.
Lua, outside of ComputerCraft, has a great function that does all these things, called require. Since we're dealing with ComputerCraft, however, this function is not available to us. As such, we're going to implement a similar function of the same name. Require is a rather simple function, it essentially just needs to load a file regularly, give that file access to require by using a custom environment, and then cache its result, so we don't have to load it again. Here's an example implementation, that you can simply drop in your main file:

local require

do
  local requireCache = {}

  require = function(file)
	local absolute = shell.resolve(file)

	if requireCache[absolute] ~= nil then
	  --# Lucky day, this file has already been loaded once!
	  --# Return its cached result.
	  return requireCache[absolute]
	end

	--# Create a custom environment so that loaded
	--# source files also have access to require.
	local env = {
	  require = require
	}

	setmetatable(env, { __index = _G, __newindex = _G })

	--# Load the source file with loadfile, which
	--# also allows us to pass our custom environment.
	local chunk, err = loadfile(absolute, env)

	--# If chunk is nil, then there was a syntax error
	--# or the file does not exist.
	if chunk == nil then
	  return error(err)
	end

	--# Execute the file, cache and return its return value.
	local result = chunk()
	requireCache[absolute] = result
	return result
  end
end

So let's put this function to use. Let's say we wanted a file which has a bunch of utility functions. We would create a file called utils.lua, which will return a table containing our functions. That could look something like this:

local utils = {}

function utils.clamp(val, min, max)
  return math.min(math.max(val, min), max)
end

return utils

With our shiny new function, using this file is easy!

local utils = require("utils.lua")
print(utils.clamp(2, 3, 8))
print(utils.clamp(5, 3, 8))
print(utils.clamp(9, 3, 8))
--# Prints 3, 5 and 8.

And check this out:

utils.test = "Hello!"

local otherUtils = require("utils.lua")
print(otherUtils.test)
--# Prints "Hello!".

After requiring our utils.lua file again, you'll notice that it returns the same table as the first time we called it! That's our cache at work.

Anyway, I hope this tutorial has helped you. Feel free to ask questions in a reply!
houseofkraft #2
Posted 01 December 2016 - 09:55 PM
Couldn't You just do os.loadAPI?
Lignum #3
Posted 01 December 2016 - 10:25 PM
Couldn't You just do os.loadAPI?

You could, but os.loadAPI loads your files into the global table, which is bad practice, especially when you're loading things that aren't APIs.
Mao Zedong #4
Posted 08 December 2016 - 05:44 PM
This is brilliant. This makes it significantly easier to work on large modular projects. Big fan of the node-style require once! Great work, thanks for this.