Using External Modules

There's often confusion as to what exactly happens when external modules are "required" into your code, which leads to further confusion and unexpected behavior when it comes to things such as Composer scenes or even custom modules of your own.

This tutorial will guide you through a series of exercises (with explanations) that should illustrate exactly how modules work in Lua, so you get a full understanding of when (and which) code in your modules is executed.

Including External Modules

In its simplest form, an external module is simply a Lua file that returns something, most likely a table. It can almost be thought of as a function definition, but it's contained in a file all to itself.

Here's a really simple module, example1.lua, which simply prints a statement to the console and returns an empty table:

-- example1.lua
 
local M = {}

print( "example1.lua has been loaded" )

return M

Now, in a different module — let's just use main.lua — require the example1.lua module and see what happens (note that all code examples in this tutorial assume that the custom external modules are placed in the root level of your project folder, alongside main.lua).

-- main.lua
 
local ex1 = require( "example1" )

ex1.testvar = "Hello World!"

As expected, upon execution of the first command, the variable ex1 becomes equal to the empty table (M) which we created and returned in example1.lua, and the words example1.lua has been loaded appear in the console.

In the next command, we assign a trivial property, testvar, to this ex1 table.

Pay close attention now since this gets more tricky. Let's require example1.lua in an another module, scene1.lua, after we have already required it within main.lua:

-- scene1.lua (previous "main.lua" still applies)

local examp1 = require( "example1" )

print( examp1.testvar )

This time — in scene1.lua — when you require the example1.lua module, the words example1.lua has been loaded do not appear in the console. However, when you print the value of examp1.testvar, Hello World! is output which means that the testvar property exists! What can we learn from this? Read on...

The "package.loaded" Table

The reason why example1.lua has been loaded did not appear in the console is because the module was already loaded by main.lua. Essentially, when a module is loaded (required), its code is executed from top to bottom as usual, and the return value of the module is stored in a global table called package.loaded. From that point on, the module is considered "loaded" and it can't simply be re-loaded fresh by requiring it again.

Let's explore this in a bit more detail:

Executing Module Code

There are two ways to get code within a module to run:

  1. Put the code inside a function (within the module) and call it like a normal function.

  2. Remove the module from the package.loaded table and re-require it (generally not recommended).

Here's an example of the first scenario:

-- example2.lua
 
local M = {}

print( "example2.lua has been loaded" )
 
M.hello = function()
    print( "Hello World!" )
end

return M
-- main.lua

local ex2 = require( "example2" )  --> example2.lua has been loaded

ex2.hello()  --> Hello World!
-- scene1.lua

local examp2 = require( "example2" )

examp2.hello()  --> Hello World!

As you can see, we required example2.lua in two different modules, main.lua and scene1.lua. The first print() statement — the one outside of any functions — occurs only once (the first time the module is required within main.lua). In contrast, the second print() statement — the one within the M.hello function — is executed each time that function is called.

What is the lesson learned? Essentially, if you want to run code within a module more than once, or if you don't want to run it immediately upon requiring the module, you should wrap it within a function and "attach" the function to the table which you return at the end of the module.

Important

Properly "attaching" module functions to the returned table is essential to gain access to them. You can not simply include the function within the module as a local function and then expect access to it from another module. Notice the difference:

local M = {}
 
-- This function WILL be accessible outside of this module, wherever the module is required
M.hello = function()
    print( "Hello World!" )
end

-- This function will NOT be accessible outside of this module, only within it locally
local hello = function()
    print( "Hello World!" )
end

return M

Removing Modules

As outlined above, you require a module like example2.lua using the following syntax:

local ex2 = require( "example2" )

With this, the return value of example2.lua is effectively stored in the package.loaded table under:

package.loaded["example2"]

Thus, if you want to "un-require" a module and clear it from memory entirely, simply assign nil to this reference as follows:

package.loaded["example2"] = nil

At this point, the module will be cleared from memory and all of its associated function(s) will cease to exist.

Conclusion

Hopefully this tutorial has given you a solid foundational understanding of Lua modules, how to include them, and how to call functions that exist in one module from another module. Conveniently, these rules are universal and apply to any module you may be dealing with, whether it's your own custom-made module, a module you downloaded, or even built-in Solar2D modules.