Important News: 26/10/2016 - Call for Presentations to Lua Devroom at FOSDEM 2017
Important News: 04/05/2016 - Community news #2
Important News: 11/12/2015 - Blog opening and contribution guide

An Introduction to Metatables

By Jeremy Clarke Jun 12 2016 13:06 General Reblogged Comments

Hi folks, this post aims to offer a clear introduction to the topic of metatables in Lua for those who are not yet familiar with them. I originally wrote this for the forums of PICO-8, a 'fantasy console' with limitations inspired by classic 8-bit computers, which uses a modified flavour of Lua 5.2.

Without further ado, let's go!

A table is a mapping of keys to values. They're explained quite well in the PICO-8 manual and the Lua reference manual so I won't go into more detail. In particular you should know that t.foo is just a nicer way of writing t["foo"] and also that t:foo() is a nicer way of calling the function t.foo(t)

A metatable is a table with some specially named properties defined inside. You apply a metatable to any other table to change the way that table behaves. This can be used to:

  1. define custom operations for your table (+, -, etc.)
  2. define what should happen when somebody tries to look up a key that doesn't exist
  3. specify how your table should be converted to a string (e.g. for printing)
  4. change the way the garbage collector treats your table (e.g. tables with weak keys)

Point #2 is especially powerful because it allows you to set default values for missing properties, or specify a prototype object which contains methods shared by many tables.

You can attach a metatable to any other table using the setmetatable function.

All possible metatable events are explained on the lua-users wiki:
>>> list of metatable events <<<

which is, as far as I'm aware, the best reference for everything that metatables can be used for.

And that's really all you need to know!

Vectors Example

I'll now demonstrate how metatables could be used to make a "2D point/vector" type, with custom operators.

-- define a new metatable to be shared by all vectors
local mt = {}

-- function to create a new vector
function makevec2d(x, y)
    local t = {
        x = x,
        y = y
    }
    setmetatable(t, mt)
    return t
end

-- define some vector operations such as addition, subtraction:
function mt.__add(a, b)
    return makevec2d(
        a.x + b.x,
        a.y + b.y
    )
end

function mt.__sub(a, b)
    return makevec2d(
        a.x - b.x,
        a.y - b.y
    )
end

-- more fancy example, implement two different kinds of multiplication:
-- number*vector -> scalar product
-- vector*vector -> cross product
-- don't worry if you're not a maths person, this isn't important :)

function mt.__mul(a, b)
    if type(a) == "number" then
        return makevec2d(b.x * a, b.y * a)
    elseif type(b) == "number" then
        return makevec2d(a.x * b, a.y * b)
    end
    return a.x * b.x + a.y * b.y
end

-- check if two vectors with different addresses are equal to each other
function mt.__eq(a, b)
    return a.x == b.x and a.y == b.y
end

-- custom format when converting to a string:
function mt.__tostring(a)
    return "(" .. a.x .. ", " .. a.y .. ")"
end

Now we can use our newly defined 'vector' type like this:

local a = makevec2d(3, 4)
local b = 2 * a

print(a)      -- calls __tostring internally, so this prints "(3, 4)"
print(b)      -- (6, 8)
print(a + b)  -- (9, 12)

Pretty neat right?

Object Orientation

I mentioned that metatables can be used to define what should happen when a key lookup fails, and that this can be used to create custom methods shared by many tables. For example we might want to be able to do this:

a = makevec2d(3, 4)
a:magnitude()  -- calculate the length of the vector, returning 5

In Lua this is not always necessary, for example, we could define an ordinary function to do the job for us:

function magnitude(vec)
    return sqrt(vec.x^2 + vec.y^2)
end
magnitude(a)  -- returns 5

In fact, for PICO-8 I would recommend that approach, because it's as efficient as you can get, and it uses the least number of tokens (PICO-8 cartridges are limited in code size).

But I think it's educational to see how metatables can make it possible to use Lua in a more OOP style.

First off, we define all our methods in a table somewhere. Note, you can define them in the metatable itself (this is a common convention), but I'll put them in a different table to prevent confusion.

local methods = {}
function methods.magnitude(self)
    return sqrt(self.x^2 + self.y^2)
end

The __index property of a metatable is referred to when you try to look up a key 'k' which is not present in the original table 't'.

If __index is a function, it is called like mt.__index(t, k)
If __index is a table, a lookup is performed like mt.__index[k]

So we can add the magnitude function, along any other methods we may have defined, to all our existing vector objects by simply setting the __index property to our table of methods:

mt.__index = methods

And now, as we wanted, we can call a:magnitude()
Which is a shortcut for a.magnitude(a)
Which is a shortcut for a["magnitude"](a)

Hopefully given all this information, it's clear what's happening: We never defined a magnitude property in 'a', so when we try to lookup the string "magnitude", the lookup fails and Lua refers to the metatable's __index property instead.

Since __index is a table, it looks in there for any property called "magnitude" and finds the magnitude function that we defined. This function is then called with the parameter 'a' which we implicitly passed when we used the : operator.

Well, that's it from me! I hope somebody finds this post useful, and please let me know if there is something you don't understand, or something that I left out or could have explained better. If you'd like to see more examples of metatable usage and OOP, I recommend chapters 13, 16 and 17 of Programming in Lua.


Community news #2

By Etiene Dalcol May 04 2016 23:46 General Comments

Meet-ups and conferences!

Next

luaconf logo

Past

News

Featured Releases


Continuous Integration with Lua

By Enrique García Cota Feb 16 2016 17:40 General Reblogged Comments

I recently gave a talk in FOSDEM, which is a great event if, like me, you are into open source.

Direct link to slides

You can watch the whole talk in video format. If you prefer a text-based approach, read on.

If you want to know what Continuous Integration is, or why it is useful, you could start by reading the article I wrote about that.

I manage several open source long-lived Lua projects. I have continuous integration in almost all of them, and it has been of great help. What I am about to explain works for my kind of projects, but it can be adapted to other options.

a) The environment

Continous Integration takes place in a server. Since we're talking about Lua here, the server must have a way to install some version of Lua, and some version of Luarocks. This is what I call "the environment". I want to test my libraries in Lua 5.1, 5.2 & 5.3, as well as LuaJIT 2.1 & 2.2. So I need my CI server to install all that.

I choose travis-ci as my main CI server. It is free for open-source tools, and it integrates very well with github. It is also very easy to configure: you just need to add a file called .travis.yml on the root of your repo, and travis will use it to run all the checks.

Travis supports a wide variety of languages. Setting up the environment for the supported languages is incredibly easy. For example, the following .travis.yml file will install 4 versions of ruby:

# .travis.yaml
language: ruby

rvm:
  - '1.9.3'
  - '1.8.7'
  - 'rbx-2.1.1'
  - 'ree'

Lua, unfortunately, is not one of the supported languages, which means it requires more work. I have seen lots of people going this route:

# .travis.yaml
language: C # does not really matter

env:
  - LUA=lua5.1
  - LUA=lua5.2
  - LUA=lua5.3
  - LUA=luajit2.0
  - LUA=luajit2.1

before_install:
  - source install_lua.sh

What the env section of that file does is setting up environment variables. It is basically saing: run one time with the LUA environment variable equals to "lua5.1", another time with "lua5.2", etc. In travis, each of these "times" it called a job. The before_install step, which is repeated at the beginning of every job, executes a script file called install_lua.sh, which in turn will install the appropiate version of Lua depending on the value of the LUA environment variable.

This solution works. But there are several things I don't like. install_lua.sh is usually a big file written in shellscript, usually several hundreds lines of code, which require maintenance. It also must be included on each repo which uses this approach for CI.

Fortunately, a better solution was presented to me. And it came from an unlikely place: Python!

There is this Python project called hererocks. Hererocks is a python script which can install any of the major versions of Lua and LuaJIT in a folder, with no "global" dependencies. It's maintained by Peter Melnichenko, and since Python is supported by travis, we can set up the environment like this:

# .travis.yaml
language: python
sudo: false

env:
  - LUA="lua=5.1"
  - LUA="lua=5.2"
  - LUA="lua=5.3"
  - LUA="luajit=2.0"
  - LUA="luajit=2.1"

before_install:
  - pip install hererocks
  - hererocks lua_install -r^ --$LUA
  - export PATH=$PATH:$PWD/lua_install/bin

What this does is: since Python is supported by travis, we can set it up in the language section. This makes Python's package manager, pip, available. Then we use pip to install hererocks. And then we use hererocks to install the version of Lua which is required for each job in a folder called lua_install.

b) Specs

As I said before, specs are "extra code which makes sure that your regular code works as you think it should". A usual term to refer to them is automated tests. I prefer specs because it is a bit more specific (there are lots of "tests" which can be "automated").

To implement my specs I use the library busted, by olivinelabs.

A quick taste of how specs look like in busted. Here's an extremely simple Lua library:

-- mylib.lua

return {
  add = function(a,b) return a+b end
}

And here's how we could write the spec for that library in busted:

-- spec/mylib_spec.lua

local mylib = require 'mylib'

describe('mylib', function()
  it('adds numbers', function()
    assert.equal(5, mylib.add(2,3))
  end)
end)

busted declares several global variables: describe makes groups of specs. it creates one spec. assert is monkeypatched so in addition of its usual role it can be used to specify assertions. When these assertions are met, the specs containing them are passed (green). If any assertion on a spec "fails", the spec becomes "failed" (red).

Here's how you install & execute busted in travis.yml:

# .travis.yml

install:
  - luarocks install busted

script:
  - busted --verbose

The --verbose option gives a little more information when a spec fails.

Just by adding this to your .travis.yml, you should be able to "run your specs" through Travis. It integrates with github, and will give you some niceties if you use it as a platform: it will run the specs automatically every time anyone pushes code to any of the branches, or sends a pull request. It will also mark all the commits and pull requests with a green flag (when the specs pass) or a red cross (when they don't).

Travis Github integration

c) Coverage

Now that you have code which tests that your code does what you think it should do. What else is there?

One of the things that you can do is getting the coverage of your specs.

Coverage is a number - usually a percentage. It tells you how many of your lines where executed when the specs were run. So, if coverage is 100%, that means that all the lines in your code have been executed at least once. If your coverage is 50%, only half of your where executed. Etc.

A tool which I find really useful is the coveralls.io website. It is not a coverage calculation tool. But once you get your coverage data to it, it can present it in a very human-friendly way. And it doesn't limit itself to showing a single number; it presents multiple reports, per build, job, file and line. That last one is the one which I like the most:

coveralls file report

The picture above is coveralls.io showing the source code of one of my libraries, middleclass.

  • The lines in wite(ish) are blank lines, comments, and lines with syntactic value but no semantic value (such as the end finishing an if or a function). These are not interesting for the coverage, and are ignored.
  • The green lines are lines which have been executed at least once when the specs where run. Notice the number to the right of each one: that's the number of times each line has been executed.
  • The red line is did not get executed at all during the specs. This usually means that I am missing some spec for testing it (it could also mean that my code has lines which are never executed).

In addition to all the reports, coveralls.io also has "github hooks" - it will "turn red" the pull requests which lower the coverage, and "green" the ones which make it bigger (or the same).

So, how do we get the coverage info appearing in coveralls.io? In my case, I generate that info in travis, and then upload it to coveralls.

The standard Lua tool for generating coverage information is Luacov. Luacov can be used to generate a file with almost all the information coveralls.io needs. A second tool, called luacov-coveralls, translates this local file into a format coveralls understands, and does the upload.

Both luacov and luacov-coveralls are installable via luarocks, so setting them up in travis is very simple. We do something similar to this:

# .travis.yml

install:
  - luarocks install luacov
  - luarocks install luacov-coveralls

Once we have the local file generated by luacov, sending it to coveralls is also very easy. Since I want to send the coverage information only when the specs run correctly, I call luacov-coveralls on the after_success section of .travis.yml:

# .travis.yml

after_success:
  - luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install

Notice that I add a -e parameter to luacov-coveralls. This makes it ignore anything happening inside the lua_install folder. If I didn't do this, the coverage information would include busted's source code, too. This is only necessary because I am using hererocks to set up the environment.

The only missing part is generating the luacov file. It turns out that busted already has a command-line option for it, called --coverage. Add it to busted, push the changes to travis, and it will start sending information:

# .travis.yml

script:
  busted --verbose --coverage

If you are not using busted, luacov can also be included when runing Lua or LuaJIT from the command line. Check luacov's documentation for more information.

d) Static analysis

Contrarily to specs or coverage studies, Static Analysis occurs *without executing the code it analyzes. It turns out that lots of useful information can be extracted from the source code by just reading it.

For static analysis, I use luacheck. This great tool detects common defects in Lua code:

  • Declared but unused variables or function parameters
  • Local variables obscuring other similarly named variables in their scope
  • And the one which probably is more useful, it detects global variables.

Luacheck can be installed with luarocks and executed from the command line. Adding it to travis is straightforward (I run it before I run the specs, on the script namespace):

# .travis.yml

install:
  - luarocks install luacheck

script:
  - luacheck --std max+busted *.lua spec

The only interesting bit is the --std option. The value max means "accept all the global variables which are used in any Lua version". This means that pairs, ipairs, etc will not be detected as variables. But so will pack (even if it isn't a global variable in Lua 5.3) or jit (even if it's not available in vanilla Lua). If you want to detect global variables with more detail, you might want to change this setting by a value adapted to your environment. The +busted part adds the global variables declared by busted (like describe or it).

e) All together

Here's the complete .travis.yml I use in my projects now:

# .travis.yml

language: python
sudo: false

env:
  - LUA="lua=5.1"
  - LUA="lua=5.2"
  - LUA="lua=5.3"
  - LUA="luajit=2.0"
  - LUA="luajit=2.1"

before_install:
  - pip install hererocks
  - hererocks lua_install -r^ --$LUA
  - export PATH=$PATH:$PWD/lua_install/bin

install:
  - luarocks install luacheck
  - luarocks install busted
  - luarocks install luacov
  - luarocks install luacov-coveralls

script:
  - luacheck --std max+busted *.lua spec
  - busted --verbose --coverage

after_success:
  - luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install

It's worth noting that I use this file as a template; each project might require some customization (deactivating one luacheck rule here, removing an unsupported Lua version there). I encourage you to adapt it to your needs.

Thanks for reading, and happy hacking!


RSS

Subscribe to Lua.Space by Email