Lua for Programmers Part 3: More Advanced Concepts

Posted on September 07, 2012 in Tutorials

If you haven't read part one and two already, I'd highly recommend doing so. For reference, here's a list of the parts in this series:

Blocks and Scope

In part one I stated that Lua has "braces of a sort in the form of keywords like then and do." Much like a language with C-based syntax, these "braces" represent blocks, which are essentially sequences of statements. An example of some blocks in Lua:

if x then
  stuff()
  moreStuff()
end

for i = 1, 10 do
  local x = "foo"
end

function foo(x, y)
  -- ...
end

-- explicit block
do
  local x = 3
  local y = 4
end

Conditionals, loops, and functions are all blocks. You can explicitly define a block as shown by the last example.

Lua uses lexical scoping, which means that every block has its own scope:

x = 5 -- global

function foo()
  local x = 6
  print(x) -- 6
  
  if x == 6 then
    local x = 7
    y = 10 -- global
    print(x) -- 7
  end
  
  print(x, y) -- 6, 10
  
  do
    x = 3
    print(x) -- 3
  end
  
  print(x) -- 3
end

foo()
print(x, y) -- 5, 10

This example demonstrates the effect of variable shadowing. It also demonstrates how global variables can be defined at any scope if the local keyword is absent and no local variables exist by that name (as is the case with y).

Collapsing Blocks

Lua allows you to collapse blocks onto one line:

function foo(x) return x * 5 end
if x then print(x) end
do foo() end

You'll see this used a lot in Lua code.

More On Functions

Default Arguments

As mentioned in part one, Lua doesn't directly support default arguments, but there are ways of getting the same behaviour. If a value isn't supplied for a function argument it will default to nil, as all undefined variables do. Therefore:

function func(x, y, z)
  if not y then y = 0 end
  if not z then z = 1 end
  -- code
end

A more common method is to use the or operator instead:

function func(x, y, z)
  y = y or 0
  z = z or 1
end

Table Syntax

There's an interesting syntax for calling functions that take a table as their only argument:

function foo(t)
  return t[1] * t.x + t[2] * t.y
end

foo{3, 4, x = 5, y = 6} -- 39

That function call is equivalent to:

foo({ 3, 4, x = 5, y = 6 })

Variable Arguments

Functions support the capturing of a varying number of arguments:

function sum(...)
  local ret = 0 
  for i, v in ipairs{...} do ret = ret + v end
  return ret
end

sum(3, 4, 5, 6) -- 18

To do so, just put ... at the end of your argument list. You can access members of the list of arguments by putting them in a table ({...}) or through the select function:

function sum(...)
  local ret = 0
  for i = 1, select("#", ...) do ret = ret + select(i, ...) end
  return ret
end

To put it briefly, select returns all values after the specified index, or the length of the list if "#" is provided instead. See the documentation for more.

Finally, just to be clear, ... isn't a data structure of any kind, it's merely a list of values, like what you get from functions returning multiple values.

self

Lua gives us a neat piece of syntactic sugar for calling and defining functions. When you have a function inside a table, you can do stuff like this:

t = {}

function t:func(x, y)
  self.x = x
  self.y = y
end

t:func(1, 1)
print(t.x) -- 1

The definition and call translate to:

function t.func(self, x, y)
  self.x = x
  self.y = y
end

t.func(t, 1, 1)

Although this is unnecessary in the example above, it becomes incredibly useful when mixed with metatables to implement things like object-oriented programming.

Loading Files

An important part of any language is how code is separated into multiple files. As you may know, Lua files generally end with the .lua extension. There are a few ways to load and run a file from inside a Lua script. Two of those ways are dofile and loadfile:

dofile("test.lua")
loadfile("test.lua")()

func = loadfile("test.lua")
func()

All of those methods are essentially equivalent. Lua files are loaded as functions; this explains why loadfile returns a function. In addition to doing the job of loadfile, dofile also calls the function (if no errors occurred when compiling the file).

This method of loading files as functions opens up some interesting possibilities:

local message = "Cheese for everyone!"
local t = { 1, 2, 3, 4, 5 }
print(message)
return t

As you can see, files can return values and define local variables. Here's how we might go about loading such a file:

x = dofile("cheese.lua")

x would be set to t, and the message from cheese.lua would be printed to the console.

Package System

A much better and more common way to load files is via the package system:

require("cheese")
require("folder.subfolder.file")

The require function takes a path to a Lua file and searches for it in a number of ways. The manual explains the whole process, so I'll just tell you what you need to know. require replaces each dot with the directory separator ("/" on Unix systems). It then looks for this file in the locations specified by package.path, which includes the current directory.

Once the file's been loaded, require adds the path you specified to the package.loaded table. require won't load any path found in this table, ensuring that files will only be loaded once.

That's the essence of the package system, but there's a lot more to it, so be sure to check out the documentation.

Executing Strings

If you want to compile a string as a function within a Lua script, loadstring does the job:

loadstring("print('hello')")()

It works the same way as loadfile. This means that you can have local variables and return values inside the strings too.

Metatables

Metatables are a very powerful and flexible feature of Lua that allow you to modify how tables behave. With them you can implement object-oriented programming, advanced data structures, and a lot more. Since I've already written a rather comprehensive tutorial on the subject, I'll direct you to that instead of explaining metatables here. Another source you could read is, of course, Programming in Lua.

Conclusion

As with the last part, there are many things I didn't cover that you might want to check out yourself; some examples are iterators, coroutines, and error handling. Be sure to check out the final part of the series too.

As always, if you have any feedback, I'd love to hear from you.


Tagged in Lua, programming
blog comments powered by Disqus