How repo modules use the “require” statement

Introduction

In general we like to avoid introducing global functions and variables into the Lua namespace. The benefit of this is modularity, we can use the same (local) function names in different modules without causing any conflicts. If we create global functions there a risk of the same name being used by multiple modules and/or user code and causing conflicts (in fact conflicts are a certainty it just a matter of time).

For our iNTERFACEWARE modules we have found three effective patterns for writing modules, all of which avoid using global functions or variables. Basically we use local functions and variables within each module, and return a variable (or function) at the end of the module that acts as the module interface.

There are the three patterns we implement:

  1. Returning a local table: Which contains the functions for the module.
  2. Returning a function: Used when a module is “single use” (there is only one function)
  3. Modifying the global namespace: Used to change or extend behaviour (of existing namespaces)

These are described in detail in the sections below.

Tip: Using the Find Global Symbols module makes it much easier to find (and then remove) unwanted global symbols.

Returning a local table [top]

Most modules will contain multiple related functions. In these cases we follow the pattern of returning a table that contains the functions.

This is the pattern, using a module called foo:

-- Define foo as a table
local foo = {}

-- function bar is a member of the table foo
function foo.bar()
end

-- function bar2 is also member of the table foo
function foo.bar2()
end
-- at the end of the module we return the foo table.
return foo

To use this module the best practice is to assign the value returned from require 'foo' into a local variable. The variable will be assigned the table foo. For example:

-- Define local variable foo and assign value returned from require 'foo'
local foo = require 'foo'

function main(Data)
   -- Now we can call the bar function on the foo module, using the foo variable.
   foo.bar() 

   -- We can also call bar2 in the same way.
   foo.bar2()
end

Why is it important to put the keyword “local” before the require 'foo'? This is because it means the foo variable is only defined in the scope of the lua file that you invoked require in. This means there is no risk of causing an unexpected side effect in another module which might be using the foo variable for another purpose or using a different foo module.

If you do trace(foo) and expand the dialog, then you can see that foo is just an ordinary Lua table that happens to contain functions.

What do you do if you want to include two modules that would both normally go into the ‘foo’ variable? Well the beauty of this pattern is that we can name the variable anything we like, for instance:

-- Assign table value from require 'foo' into foo2 variable
local foo2 = require 'foo'

function main(Data)
   -- Now to call the bar function we prefix is with foo2
   foo2.bar()
end

Returning a function [top]

Some modules are single purpose, so they only contain a single function. In these cases we usually follow the pattern of returning a single function (rather than returning a table containing just one function).

Here is an example of the pattern:

-- Notice that function Foo is declared "local" - it will not appear in the
-- global namespace of the Lua engine.
local function Scrub()

end
-- at the end of the module we return the Scrub function.
return Scrub

The benefit of this pattern is that it gives us even more flexibility in how we use this function. We can assign the function to an arbitrary local value within a module, or we can even assign it to a member of a pre-existing table that may already contain other functions like the hl7 namespace.

Here’s one example:

-- We assign the scrub function into the scrub member of the hl7 table: 
hl7.scrub = require 'scrub'
-- We could have written it like this:
hl7['scrub'] = require 'scrub'

function main(Data)
   -- Call the scrub function
   hl7.scrub()
end

And here’s another:

local ScrubADubDub = require 'scrub'

function main(Data)
   -- Just call the function with the variable name we assigned it to
   ScrubADubDub()
end

Modifying the global namespace [top]

Some modules change or extend the behaviour of our builtin Lua modules (i.e., the existing global namespaces). To change behaviour modules simply use a function with the same name in the same namespace (i.e., net.http.get) to override the original behaviour. To extend behaviour they simply add new functions to the namespace (i.e., net.http.myNewFunction).

An example of a module that modifies existing behaviour is net.http.cache, which modifies the pre-existing net.http.* functions to add caching ability to them.

An example of a module that extends behaviour is stringutil, which adds functions to the the build in node and string namespace tables. This is more invasive (than net.http.cache) since it impacts on the methods that we see on “node tree” objects (HL7, XML, Database table objects) and built in Lua strings.

We tend to be very very careful with these types of modules! There really has to be a big win in terms of usability to make this worth it because it does introduce risks of breaking code that would otherwise work if these global dependencies were not introduced.

Leave a Reply