A Simple Intro to Metatables and Classes

Metatables

Though I use metatables for a number of reasons, my general opinion on metatables is to not use them just to use them.

The use and construction of metatables can be highly complex. The simplest metatable key to understand is the __index key.

The __index key basically is used to lookup values that are not found in the base table. In a sense, using OOP terminology, it “extends” a table. When using setmetatable a new (unique) table is returned with the added lookup, so this does position itself to being useful when working with Class instances.

A very, very simple example:

local t1 = {
  color = "Blue"
}

local t2 = {
  name = "Jimmy",
  age = 20
}

local t3 = {
  name = "Jenny",
  age = 32
}

local tbl1 = setmetatable(t2, { __index = t1 })
local tbl2 = setmetatable(t3, { __index = t1 })

print(tbl1.color) -- Blue
print(tbl1.age) -- 20

print(tbl2.color) -- Blue
print(tbl2.age) -- 32

When printing the color property, which does not exist in either t2 or t3, Lua will use the __index key to see if that property exists. Since it does, the value is returned.

Whats interesting about this, is that you can actually change the color property and it will only affect the table it is changed on:

tbl1.color = "Green"

print(tbl1.color) -- Green
print(tbl1.age) -- 20

print(tbl2.color) -- Blue
print(tbl2.age) -- 32

This of course lends itself to some interesting use cases, especially when functions are involved. This is where metatables tend to come into the OOP conversation.

Classes

Something I think that is important to point out is that metatables are not intrinsic to OOP. A Lua table with properties and functions is a form of a Class (though a static one in OOP terms).

A Class is generally used to create “instances” of the Class. These instances are usually self-referential, hence the heavy use of the “self” keyword.

For example, the following two examples produce the same results:

With metatable:

--#############################################################################
--# User Class (metatable)
--#############################################################################
local _M = {}
local mt = { __index = _M }

--#############################################################################
--# Instance Methods
--#############################################################################
function _M.setName(self, name)
  self.name = name
end

function _M.getName(self)
  return self.name
end

--#############################################################################
--# Instance Creation
--#############################################################################
local User = {}

function User.new()
  return setmetatable({}, mt)
end

return User

Without metatable:

--#############################################################################
--# User Class
--#############################################################################
local _M = {}

function _M.new()
  --#############################################################################
  --# Instance Creation
  --#############################################################################
  local User = {}
  
  function User.setName(self, name)
    self.name = name
  end

  function User.getName(self)
    return self.name
  end

  return User
end

return _M

The nice thing about Classes is that we can reuse code, yet keep separate state within the instances (also referred to as objects). With Lua we get the convenience of specifying the “self” using the colon syntax ( : ) so the usage tends to look like:

local User = require("User")

local user1 = User.new()
local user2 = User.new()

user1:setName("Sally")
user2:setName("John")

print(user1:getName()) --Sally
print(user2:getName()) --John

Metatables and Classes

As demonstrated above, metatables are not required to emulate OOP like practices. So why would you use them?

One use case for using metatables is that they can provide a nice interface when creating your instances, allowing you to add additional properties in a simple manner.

-- User.lua
local User = {}

function User.new(props)
  return setmetatable({}, { __index = props })
end

return User

Usage:

local User = require("User")

local user1 = User.new({name="Bob"})
local user2 = User.new({name="Carol"})

print(user1.name) -- Bob
print(user2.name) -- Carol

The alternative (without metatables) might look like:

local _M = {}

function _M.new(props)
  local User = {
    name = props.name
  }

  return User
end

return _M

Which may not seem like too much of a big deal until you think about this:

local User = require("User")
local user1 = User.new({name="Bob", age=20, color="Blue"})

Using the metatables version, no code needs to be changed to handle the additional properties:

print(user1.name, user1.age, user1.color) -- Bob, 20, Blue

Whereas the alternative would require an update to the code:

...

function _M.new(props)
  local User = {
    name = props.name,
    age = props.age,
    color = props.color
  }

  return User
end

...

Using the metatables example of creating Class instances (show earlier) along with passing properties can allow for simple and very dynamic Class structures.

The great thing about Lua is that there are often multiple approaches to the same problem. And while metatables are very useful, that doesn’t mean they need to be used for the sake of using them.

Knowing what tools to use, and when, is what good programming is all about.

-dev

Leave a Reply

Your email address will not be published. Required fields are marked *