Lua Metamethod Assignment Discovery

Sure, you probably know what a metatable is. A table that you attach to another table that contains metamethods which fire at certain occasions. But a novice to these metatables probably hasn't seen a real example of one. And that's where this page comes in handy!

There are 19 metamethods you can use in Roblox Lua. You'll get to see one example of each.

Table-access

expression invokes
a.b __index(a, "b")
__index["b"]
a.b = c __newindex(a, "b", c)
__newindex["b"] = c

__index

The metamethod fires when an index in a table is read from, if the value for that index is nil. If a value is returned from , that value is used as the result of the indexing. The function never invokes the method.

When you try to index the fourth and fifth indices, which are nil, it fires the metamethod, giving 9 and 10

Often, the metamethod is used to lookup keys in another table. This can be useful to look up default values. Due to this frequent usage, can be set to a table. When set to a table t, will return , where is the index attempting to be indexed.

__newindex

The __newindex metamethod is similar to the __index metamethod, except it fires when you try to set the value at an index with a value of nil. Note that it only fires for keys whose value was nil, not any others, as the name "newindex" suggests. The function never invokes the metamethod, which can make it useful for setting keys inside __newindex without causing infinite recursion.

The __newindex metamethod in this example fires when you try to set a nil index's value, and checks if the value is a number. If it is, then it will print the number multiplied by 2. When you set the 6th index to 50, it prints 100. Nothing is output when the second assignment runs because the key was not new.

Arithmetic

expression invokes
a + b __add(a, b)
a - b __sub(a, b)
a * b __mul(a, b)
a / b __div(a, b)
a % b __mod(a, b)
a ^ b __pow(a, b)
-a __unm(a)

Relational

expression invokes
a == b __eq(a, b)
a ~= b not __eq(a, b)
a < b __lt(a, b)
a > b __lt(b, a)
a <= b __le(a, b)
a >= b __le(b, a)

__eq

The __eq metamethod fires when two tables are compared using the == operator.

The __eq metatable will also automatically fire if you use the ~= operator (not equal to). It will simply run the metatable with the not operator, effectively returning the opposite of what it normally would.

So, if you changed to , it would print instead of .

Note that for Lua to rely on __eq the values must have the same metatable and basic type (table/userdata/etc.) in other cases, i.e. a table and another random table, or a userdata and a table, the expression will always result in .

__lt

The __lt operator fires when two tables are compared using the < operator.

In this example, both tables are summed. If the sum of the first table is less than the sum of the second table, it returns true. Otherwise, it returns false.

The __lt metamethod will also automatically fire if you use the > (greater than) operator. It will simply run the metamethod with the arguments reversed.

So, if you changed to , it would still print .

__le

The __le metamethod fires when two tables are compared using the <= operator.

In this example, both tables are summed. If the sum of the first table is less than or equal to the sum of the second table, it returns true. Otherwise, it returns false.

The __le metamethod will also automatically fire if you use the >= (greater than or equal to) operator. It will simply run the metamethod with the arguments reversed.

So, if you changed to , it would still print .

Miscellaneous

expression invokes
a(...) __call(a, ...)
a .. b __concat(a, ...)
tostring(a) __tostring(a)
#a __len(a)

__call

The __call metamethod fires whenever you try to fire a table like you would a function.

The __call metamethod in this example passes a parameter called arg to the table, checks if the table has an index that is the same as arg, and if it does, it prints the value at that index divided by 2. Since we're passing 3 as an argument, it checks if table has a third index, and divides its value, 30, by 2. In other words, it prints 15.

__concat

The __concat metamethod fires whenever you try concatenating a table.

In this example, the __concat metamethod checks if the value is a string, and if it is, it first concatenates all of the values in the table, and then concatenates the result to the value. Since the table's values are "H", "e", "l", "l", "o", and "," it prints Hello, World! when concatenated with, "World!"

__tostring

The __tostring metamethod does as the name suggests--it fires when you attempt to convert a table to a string. It is comparable to the tostring function.

In this example, the __tostring metamethod is used almost exactly like the actual tostring is. The tostring function is called on the table, the metamethod fires, and then each individual value in the table is converted and subsequently printed to the output.

__len

The __len metamethod is unusual, because it can only be fired by a userdata (most commonly created the newproxy function). This is because the # operator can already be ran on a table, so it seems useless to make a metamethod for it.

Notes

  • The __gc metamethod (which in Lua 5.1 only fires on userdata) was removed because it was called by backend Lua and thus had an undefined context level. This allowed users to exploit past Roblox's sandbox.

See Also

local t ={"a","b","c"}local metatable ={ __index =function(self, index)return index +5end}setmetatable(t, metatable)for i =1,5doprint(t[i])end
local defaults ={a ="hello", b ="world"}local mt ={ __index = defaults }-- equivalent to-- local mt = { __index = function(self, k) return defaults[k] end }   local x =setmetatable({}, mt)local y =setmetatable({}, mt)   x.a =10 y.b =10   print(x.a, x.b)print(y.a, y.b)
local t ={1,2,3}local metatable ={ __newindex =function(self, index, value)iftype(value)=="number"thenprint(value *2)-- set the underlying table keyrawset(self, index, value *2)endend}   setmetatable(t, metatable)   t[6]=50 t[6]=51
local t ={1,2,3,4,5}local p ={1,2,3,4,5}   local metatable ={ __eq =function(self, otherTable)local bin ={}for k,v inpairs(self)doif v ~= otherTable[k]thenreturnfalseendendfor k,v inpairs(otherTable)doif v ~= self[k]thenreturnfalseendendreturntrueend}   setmetatable(t, metatable)setmetatable(p, metatable)   print(t == p)
local t ={4,4,4}local p ={5,5,5}   local metatable ={ __lt =function(self, otherTable)local sum1 =0local sum2 =0for k,v inpairs(self)doiftype(v)~="number"thenerror("All of the table values must be numbers for this example to work.")end sum1 = sum1 + v endfor k,v inpairs(otherTable)doiftype(v)~="number"thenerror("All of the table values must be numbers for this example to work.")end sum2 = sum2 + v endreturn sum1 < sum2 end}   setmetatable(t, metatable)setmetatable(p, metatable)   print(t < p)
local t ={4,4,4}local p ={5,5,5}   local t2 ={4,4,4}local p2 ={4,4,4}   local metatable ={ __le =function(self, otherTable)local sum1 =0local sum2 =0for k,v inpairs(self)doiftype(v)~="number"thenerror("All of the table values must be numbers for this example to work.")end sum1 = sum1 + v endfor k,v inpairs(otherTable)doiftype(v)~="number"thenerror("All of the table values must be numbers for this example to work.")end sum2 = sum2 + v endreturn sum1 <= sum2 end}   setmetatable(t, metatable)setmetatable(p, metatable)setmetatable(t2, metatable)setmetatable(p2, metatable)   print(t <= p)print(t2 <= p2)
local t ={10,20,30}local metatable ={ __call =function(self, arg)if self[arg]thenprint(self[arg]/2)endend}   setmetatable(t, metatable)   t(3)
local t ={"H","e","l","l","o",","}local metatable ={ __concat =function(self, value)iftype(value)=="string"thenreturntable.concat(self,"").. value endend}   setmetatable(t, metatable)   print(t.." World!")
local t ={1,2,3,4,5}   local metatable ={ __tostring =function(self)for k,v inpairs(self)doprint(tostring(v))endend}   setmetatable(t, metatable)   tostring(t)

I have been programming in Lua for about 9 months and it seemed like a good time to pause and reflect on my experience with it. I have used various languages over the years -- Perl (soaplite.com and other projects, including my current consulting work), C (a DHCP/DNS server for Arduino and a ping-pong juggling robot), JavaScript (experiments with Google Maps and canvas), MATLAB (ping-pong juggling robot), and others, from Turbo Pascal to F# -- and it was interesting to see how Lua compares to the other languages I've worked with. I have done different types of projects in Lua: a remote debugger (MobDebug), extending a Lua IDE (ZeroBrane Studio), a mobile app (LuaRemote), several educational scripts (EduPack), and a demo of drawing on browser canvas with Lua.

I have come across several detailed lists that mention good and not-so-good parts of Lua (for example, Lua benefits, why Lua, why Lua is not more widely used, advantages of Lua, Lua good/bad, Lua vs. JavaScript, and Lua Gotchas), but I found that some of the features that tripped me or that I cared about were not listed, so I put together my own list. It is far from being comprehensive and some aspects of the language are not covered (for example, math and string libraries), but it captures the gist of my experience with the language.

Good

  • Small: 20000 lines of C code that can be built into a 182K executable interpreter (under Linux).
  • Portable: builds on any platform with an ANSI C compiler. You can see it running on almost anything from microcontrollers and Lego Minstorms NXT, to game engines, to mobile toolkits, to game consoles, to a browser (translated to JavaScript).
  • Embedded and extensible language that provides a straightforward interface to/from C/C++.
  • Sufficiently fast: performs well comparing to other languages and has a JIT compiler that noticeably improves performance on many tasks; those who may still be not satisfied with the performance, can implement critical parts in C and, given the ease of integration with C, still benefit from other good aspects. [Updated 3/9/2013] replaced shootout results that are no longer available with benchmarksgame.
  • Well documented: reference manual, book, wiki, 6-page short reference and more.
  • Friendly and enthusiastic community. Between the excellent documentation, the wiki, the mailing list, and StackOverflow, I didn't have any issues finding answers to my questions.
  • Clean and simple syntax suitable for beginners and accessible to non-programmers. Lua has borrowed most of its control syntax from Modula, the descendent of Pascal, which was widely used in education as an introductory language. I still remember using early versions of Philippe Kahn's fast and elegant Turbo PascalIDE.
  • Integrated interpreter: just run from the command line.
  • Native support for coroutines to implement iterators and non-preemptive multi-threading.
  • Incremental garbage collector that has low latency, no additional memory cost, little implementation complexity, and support for weak tables.
  • Powerful heterogeneous tables that store values of any type (except ) and can be indexed by values of any type (except ): .
  • Lexical scoping.
  • Functional programming with first class functions and closures.
  • Tail calls: .
  • Recursive functions don't need to be pre-declared: ; note this doesn't work with .
  • Functions return multiple values: . The caller can expect any number of values returned: if less than three is expected, the rest is discarded and if more than three is expected, the rest is -initialized.
  • Functions allow variable number of parameters with .
  • Tables can be "unpacked" into a list of parameters with (or in Lua 5.2): prints .
  • Manipulating environments ( and in Lua 5.1 and manipulation in Lua 5.2), which allows building sandboxes among other things.
  • Assignment to a list of variables: , , or .
  • Multiline strings (using ; can be enclosed with ) and comments ().
  • Semicolon as a statement separator is optional (mostly used to resolve ambiguous cases as in ).
  • Overloading using metatables.
  • Metaprogramming to do things from getting and modifying an abstract syntax tree to creating a new syntax for your DSL.
  • The statement has two forms: generic (loops over iterators: ) and numeric (loops over numbers: ); the numeric one supports all numeric values for steps (not just integers).
  • Syntactic sugar for function calls (, , , and ) and method calls ().
  • Simple yet powerful debug library.
  • Fast and powerful JIT compiler/interpreter (LuaJIT) which includes FFI library and is ABI-compatible with Lua 5.1 (this means that it can load binary modules compiled for Lua 5.1).

Different

  • Tables and strings are indexed from 1 rather than 0.
  • Assigning as a value removes the element from a table. This is consistent with returning for non-existing element, so it makes no difference whether the element does not exist or exists with a value of . produces an empty table.
  • No integers as a separate numeric type; the number type represent real numbers. The next version of Lua (5.3) may change that.
  • No classes; object-orientation is implemented using tables and functions; inheritance is implemented using the metatable mechanism.
  • Method calls are implemented using notation, which is the same as notation, but with evaluated only once.
  • and are the only false values; 0, 0.0, "0" and all other values evaluate as .
  • Non-equality operator is ~= (for example, ).
  • keywords used for logical operators.
  • Assignments are statements, which means there is no or .
  • No , , or similar shorthand forms.
  • No statement, although there is an explanation and a number of alternatives, like using inside the loop to break out of or a statement introduced in Lua 5.2.
  • No statement.
  • Brackets may be required in some contexts; for example, works, but doesn't; the latter needs to be specified as .
  • A control variable in a loop is localized by default and is not available after the loop.
  • Limit and step values in the numeric loop are cached; this means that in all three functions , , and are called once before the loop is executed.
  • Conditionals and other control structures require no brackets.
  • Strings and numbers are automatically converted (if string is used where a number is expected and vice versa), but not in equality/inequality comparisons: is , is , and and refer to two different keys in the table; other relational operators generate errors on comparing values of different types.
  • Both commas and semicolons can be used in table literals; both can be placed before the closing curly bracket as an optional separator: .
  • Smaller than expected number of components that are available "out of the box"; some people see this as "batteries not included". This is the other side of having a compact and portable core and is well compensated by having LuaRocks and libraries like Penlight.

Bad

  • Limited error handling support (using pcall and xpcall), although some may argue that it is sufficient and just needs some syntactic sugar and more feature support (like deterministic finalizers). The combination of and is quite powerful, especially given that can return anything (for example, a table) rather than just a string, but having constructs may be cleaner and simpler to read in many cases.
  • Global scoping by default (this has been partially addressed in Lua 5.2, which has no globals). There is a strict module that requires all global variables to be initialized. I have not had many issues caused by uninitialized globals, but still put this one into the "bad" category as I once made a mistake of calling a variable "next" and not localizing it, which caused an issue with an iterator in a completely different module as it overwrote the next function used with iterators.
  • No Unicode support (at the very least you don't get and pattern matching functions to recognize Unicode characters); there is a binding to ICU library that implements full Unicode support. See also this message and follow-ups for a good summary of what is already supported and what modifications may be required for functions.
  • Limited pattern-matching support, although the included one is still quite powerful. After using Perl for over 15 years, I miss some of the regexp features (mostly look-aheads, optional groups , and groups inside groups), but nothing that warrants the additional complexity in the implementation. Those who need more power in their regexps can use LPeg and its re module.
  • No ternary operator; several alternatives are available. I usually end up using form with the caveat that can be assigned if both and end up being .
  • No POSIX functions built-in. There is the luaposix module, but it requires compilation, which is not always an option. I didn't miss this much, but I did come across a case where I needed to get/set an environment variable, and having access to and would be convenient [Updated 6/1/2012] As miko noted in the comments, there is os.getenv, but no corresponding .
  • No class/object finalizers. Lua provides finalizer functionality through the __gc metamethod, but it is available only for userdata types (and not tables) and doesn't match the functionality provided by other languages, for example, DESTROY and END methods in Perl. [Updated 05/27/2012] There is an undocumented newproxy feature in Lua 5.1 that allows implementation of finalizers on tables; Lua 5.2 removed that feature as it added support for __gc metamethod on tables.
  • No yielding between Lua and C code: call across Lua/C boundary fails with . I happened to come across this error several times as I was doing async programming with luasocket and coroutines, but solved it using the copas module. This has been addressed in Lua 5.2.
  • No built-in bit operations in Lua 5.1. This is addressed in LuaJIT (BitOp) and Lua 5.2 (bit32), which both include bit libraries.

Ugly

  • Number of elements in a table is not easy to get and the result depends on how you do this (or what you mean by "length"). This is probably not surprising, given how powerful tables are in Lua and the fact that they support flexible indexing (by numbers and any other Lua type except ). Tables in Lua have two parts: an "array/vector" part (generated with ) and a "hash" part (generated with ); the two can be flexibly combined. returns the length of the shortest "array/vector" part (without any gaps) and returns the longest "array/vector" part (this function is removed in Lua 5.2). The "hash" part doesn't have a defined length. Both parts can be iterated over using the method, which allows you to count the number of elements in them. However, prints 4 and not 2 as one may expect, whereas prints 2. I'm sure there is a good reasonable explanation for this, but for now it is in the "ugly" bucket. [Updated 11/17/2012] As FireFly noted in the comments, in Lua 5.2 the length operator is only defined for tables that don't have holes in them.
  • statement can't be used if it's not the last statement in a block; in other words, will trigger an error or (depending on whether you have semicolon after or not). Not that anyone would want use this for anything other than debugging, but I got bitten by it couple of times. I would have put this in the "different" category, but I find it inconsistent that I can't use , but can use in exactly the same place. [Updated 5/19/2012] This also applies to statement, although in Lua 5.2 is no longer required to be the last statement in a block.
  • Only one value is returned from a function if it's not the last one in a list; for example: The related behavior of is also affected by this rule: returns three values, but returns only one value (note the extra pair of parentheses). This matches the overall simplicity of the language and is well documented, but I still find it to be a bit ugly (although ugliness as beauty is in the eye of the beholder).

Overall, I have so far enjoyed the simplicity and consistency of the language, although there are few things I wish were done a bit differently. My eight-year-old son also picked Lua syntax quickly, which reminded me a lot about my experience with Turbo Pascal decades ago.

You should get a copy of my slick ZeroBrane Studio IDE and follow me on twitterhere.

0 Replies to “Lua Metamethod Assignment Discovery”

Lascia un Commento

L'indirizzo email non verrà pubblicato. I campi obbligatori sono contrassegnati *