What and Why are Metatables?

If you’re familiar with object oriented paradigms in other languages, you may have noticed that the way Lua does OOP makes no sense. This is partly due to the way Lua is intended to be used in combination with another compiled language; you don’t have access to this half of the picture as a Roblox developer. The official documentation also doesn’t give the greatest overview of metatables either, or at least they don’t come together until the C API section of the PIL.

To understand the decision making behind the metatable system, we need to look at how Lua handles types. Lua does have types, even though they are not declared or annotated. They are nil, number, string, function, table, and userdata. Technically, Lua also makes a distinction between Lua functions and C functions, but the implementation in Roblox seems to ignore this. The important one here for our purposes is userdata.

An item of type userdata is a block of memory created by the host system in a language other than Lua, such as the Roblox engine. It is managed by Lua and can be garbage collected. Userdata allows Roblox to create custom objects for Lua code to work with. Most of the methods for working with userdata are C functions, such as FindFirstChild(). On their own, however, userdatum do not have any type information.

If this were left as-is, each type in Roblox would not know which member functions it has or whether arguments passed to those functions were the correct type. You would only be able to call functions like this: Instance.FindFirstChild(parentInstance, “childName”), and any mixed up arguments would almost certainly cause a crash.

Metatables were invented to add these object-oriented features to userdata. Either a table or userdatum can have a metatable assigned to it. A userdatum, however cannot have its metatable modified from Lua, only from C. This provides isolation and security which is of course necessary for Roblox. An ordinary table can also have its metatable locked, which is the case for some tables in Roblox.

Metatables are meant to be shared by objects (tables or userdata) of the same purpose. This means if you use metatables for object oriented programming, you should only have one metatable per kind of object. Making duplicates of the same metatable is a common Lua OOP mistake.

This usage has an important consequence. The metatable should not contain any data that are specific to one instance of the object it describes. This in turn means it should only contain member functions, not “static” functions. For example, Instance.new() is a “static” function belonging to the global table Instance, you can’t do MyPart:new().

Theres one extra layer to this which messes people up. A metatable’s only actual job is to provide customization to an object, but it does not hold member functions on its own, even though that’s how its used in the PIL and in most Roblox OOP.

Metatables have special fields starting with a double underscore which change the behavior of any table associated with it. These are all listed here. The “__index” field overrides the behavior when an object is the subject of the . : or [] operators, known as indexing. If __index points to a function, that function is called with the table and the given index as a string, that is the item inside the square brackets, or after the dot/colon. If __index instead points to a table, that table is checked for the given index as if it were the subject on the left side of the operation.

The practical application is as follows. We provide a “new” function to create an object (just a table) of type Car. This Car gets assigned the metatable car_meta, which defines the __index field as another table called mCar. We place our member functions in this last table. Then, when we try to use : on a Car to call the Beep function, it is effectively read as mCar.Beep(bobsCar). Beep is indeed a member of mCar, so the function is successfully called, and bobsCar is passed as the self argument, which is implied by the use of the : instead of the .

You’ll notice there’s not a lot of type safety going on here. On the Lua side, it is up to you to add safety checks for type. The recommended way is to use the __name field of the metatable. It isn’t really a special field like __index, but it’s how Lua chose to do it internally, so it might make your errors more descriptive.

Almost all of the time, you will see Car_meta and mCar combined. This is done by having the metatable’s __index point to itself and storing the member functions there. Please note the absolutely horrific noob trap that I have warned about via comments!

This looks like it should cause some kind of infinite loop, but it doesn’t because the Car_meta will not be its own metatable, and thus it is being indexed like a regular table. By the same token, however, don’t think you can just set a table of functions as the metatable of Car and have those work as member functions. That is, removing line 8 will not work.

Metatables allow you to do much more than just custom objects. I will cover these in a future tutorial, probably. Here are some parting warnings about using metatables:

  • Roblox’s backend differs slightly from the official Lua implementation for security reasons. It’s a good idea to double check your assumptions when accessing the metatables of built-in objects. I noticed for example that you cannot even get the metatable for Instance.

  • Metatables do not get sent when you pass a table through a remote. I believe they do get passed through bindables though. The trick in either case is to identify the intended type of the object on the receiving side and assign its metatable again there. Since the metatable should contain no state associated with the object, this should be fine and you wont lose any info. Be incredibly careful with this as you can easily create an RCE in your server code by accident. If you’re not sure, ask me to look at it.

  • Don’t try and outsmart Lua’s ordinary tables too much. You are very unlikely to get gains in performance by using metatables to change how tables get and set their members. Don’t try and recreate Java’s TreeMap class using this, is what I mean. Everything in Lua is speed constrained by the fact things are looked up by name, this is just the mandatory price you pay with a dynamic language, and its why userdata exists (too bad you can’t create your own, at least in Roblox)

  • “Can you do multiple levels of inheritance?” Yeah, but it doesn’t work the way you’d think. You need a weird trick that I may explain in the next tutorial

  • “Doesn’t Luau have types?” No, optional type annotations are not a type system. I do use the annotations in my function signatures though, to help the intellisense and to make it easier to remember how to use my functions.

3 Likes

The post should be titled Implementing OOP with metatables, since that is what you are talking about.

Metatables were created to “alter the standard behavior of operations on tables” and to do some metaprogramming. One of their possible applications is to implement OOP, although OOP can be implemented without using metatables.

It probably wasn’t necessary to mention C, PIL or userdata, since they really have nothing to do with understanding how to implement OOP. userdata is not a table.

Not really. Bindables, for some reason, also serialize data, or so I read in the forum. It could also be for security reasons between script/threads, that is, that a script does not alter the data of another script by accident.

I really disagree on this, luau types are its strong point and its raison d’être. And it is the way dynamic languages can implement types. Would you say Typescript is not typed?

3 Likes

This is not type checking:

image

This should not even start running because the types should be known to be wrong at “compile time”. Instead, it runs and errors just like Lua. That doesn’t make type annotations useless, they are still pretty useful for readability.

Would you say Typescript is not typed?

Typescript has compile time checking. If Luau had it, it would not allow this code to run; so your implication here is backwards.

I mention the C API chapters of the PIL because that’s what I had to read in order for the intended usage of metatables to make sense. You have more control over them from the API side, so RI goes into more depth about how they work there. Not sure I would recommend this, which I why I made this post, so people don’t have to go read it. I only incidentally needed to read it for work. Metatables would not be neccesary in a hypothetical Lua with only tables and no userdata, since you could just use a reserved member like __name to look up its class from some global list.

The “edit time type checking” doesn’t understand control flow, either:

image

1 Like

That is where you are wrong, type checking is “the process of checking and ensuring that the data types used in a program are consistent and comply with the rules defined by the language”. This is done using tools such as the compiler, but also others such as the editor or IDE, or some kind of third party application. In no case is it restricted to the compiler.

Remember that luau is a dynamic language, i.e. with dynamic type checking. So when compiling to bytecode it does not take type annotations into account. However luau also has a static type checking system as an additional tool, which makes use of type annotations. If you know python, it’s like mypy.

image

here the tool, the code editor, is the one that shows the static type error, not the compiler. that should be clear.

you misunderstood my point, by mentioning typescript I was referring to the fact that a dynamic language can also have static type checking.

I said it was “probably not necessary”, because I thought it was a beginner’s tutorial. Of course you have your point of view.

it does. The luau static type checking system uses inference to determine the type. In this case luau decided mightBeVector is a string because, in the static analysis, the first value mightBeVector assumed was a string. If you wanted it to be a Vector3 you should tell luau that mightBeVector is a Vector3 before assigning the string, local mightBeVector: Vector3, in which case the error would appear when assigning the string as you expect.
image

1 Like