Now that we have learned how to create class-like structures in Lua, let's
take a closer look at setmetatable to get a feel for how it works, then we will take a look at how
we add functionality to our classes by adding methods.
setmetatable
We have already seen setmetatable in action, so let's take a look to get a practical feel for what
is happening. The key points are shown in the following script:
local Widget = {
color = "blue",
style = "italic"
}
function Widget.new()
return setmetatable(
{},
{ __index = Widget }
)
end
local instance = Widget.new()
print(Widget) -- table: 0x7e8330137638
print(instance) -- table: 0x7e8330137728
print(instance.color) -- blue
print(instance.style) -- italic
As we saw in the previous section, setmetatable returns the table passed as its first parameter
(which I will call the "instance table" for clarity), and for this reason it is important that this
table be unique. Next, we print out the Widget and instance to show that they are different
tables, stored at different locations in memory. Finally, and this is the key observation - although
the "instance table" is empty (ie it has no keys defined), after it is returned from setmetable we
are able to access the color and style properties of the Widget on it.
As you might guess from the name, setmetatable sets the "instance table"'s "metatable" to its
second parameter, then returns it. Lua metatables can be used to implement a variety of
functionality, but in our case we are focused on their ability to modify how a table searches for
keys.
If you recall from the tables chapter, map-like tables reference values by name; when a name is referenced the table checks to see if that name exists as a key, then if so the value associated with that key is returned. When that name doesn't exist, however, the table returns nil.
Metatables can be used to modify this behavior, however, by adding the __index key and assigning a
value to it, which in our examples is another table. Now, when the instance looks up a name that
doesn't exist on the instance itself, it then checks to see if it exists on the __index table and
if so, uses that value. This is how Lua implements inheritance, and allows independent instances
to all access common functionality that is defined on the class.
Now, let's see how we can leverage this to add functionality to our classes. Lua supports two ways to call functions that are defined on a class, which are roughly equivalent to Python's concepts of "Class methods" and "Instance methods".
"Class method" and "Instance method" are not standard Lua nomenclature. These terms come from Python, but we find them to be a convenient way to refer to, and differentiate between, each kind of method.
Class Methods
Class methods are simple functions that are defined on the class, and can be called with respect to
the class itself or an instance of that class. For example our new and merge methods fall into
this category, where new calls merge with respect to the Widget class:
local values = Widget.merge(config)
Let's go back to our Widget and add "getter" methods that return the values assigned to the color
and style properties:
local Widget = {
color = "blue",
style= "italic"
}
function Widget.new(config)
config = config or {}
local values = Widget.merge(config)
return setmetatable(
values,
{ __index=Widget }
)
end
function Widget.merge(config)
local values = {}
for key, val in pairs(Widget) do
if config[key] == nil then
values[key] = val
else
values[key] = config[key]
end
end
return values
end
-- changes occur below this line
function Widget.get_color()
return Widget.color
end
function Widget.get_style()
return Widget.style
end
return Widget
Now, let's call them:
local Widget = require("widget")
local instance = Widget.new({style="bold"})
-- check the values assigned to the instance
print(instance.color) -- blue
print(instance.style) -- bold
-- check the values returns by our methods
print(instance.get_color()) -- blue
print(instance.get_style()) -- italic
Note that the call to instance.get_style() did not return the correct value, it returned the
default value defined on the class itself rather than the value assigned to the instance. This
highlights the reason we like to think of these as class methods - they have access to values
defined on the class itself, but not those associated with instances of that class.
In order to provide these methods access to values on the instance we need to modify them slightly, so that they accept the instance as a parameter, then we have to pass the instance to each method call:
local Widget = {
color = "blue",
style= "italic"
}
function Widget.new(config)
config = config or {}
local values = Widget.merge(config)
return setmetatable(
values,
{ __index=Widget }
)
end
function Widget.merge(config)
local values = {}
for key, val in pairs(Widget) do
if config[key] == nil then
values[key] = val
else
values[key] = config[key]
end
end
return values
end
-- changes occur below this line
function Widget.get_color(instance)
return instance.color
end
function Widget.get_style(instance)
return instance.style
end
return Widget
Now, let's test our class:
local Widget = require("widget")
local instance = Widget.new({style="bold"})
-- check the values on the instance
print(instance.color) -- blue
print(instance.style) -- bold
-- call our methods with respect to the instance
print(instance.get_color(instance)) -- blue
print(instance.get_style(instance)) -- bold
-- call our methods with respect to the class
print(Widget.get_color(instance)) -- blue
print(Widget.get_style(instance)) -- bold
Our methods now provide the functionality that we want, but it feels a bit clumsy and inefficient to have to pass the instance to the method each time we call it. Luckily, Lua provides a clever way around this, which we discuss next.
Instance Methods
Instance methods are very similar to class methods, except code inside the method has access to the
instance. Let's modify our Widget once more to convert the get_color() and get_style() methods
to instance methods:
local Widget = {
color = "blue",
style= "italic"
}
function Widget.new(config)
config = config or {}
local values = Widget.merge(config)
return setmetatable(
values,
{ __index=Widget }
)
end
function Widget.merge(config)
local values = {}
for key, val in pairs(Widget) do
if config[key] == nil then
values[key] = val
else
values[key] = config[key]
end
end
return values
end
-- changes occur below this line
function Widget:get_color()
return self.color
end
function Widget:get_style()
return self.style
end
return Widget
The key changes here are subtle, but important:
- While class methods are accessed using a
., instance methods are accessed using:. - Instance methods implicitly pass the
selfvariable into the method body, which provides access to the instance itself from inside the method body.
Let's see this in action to get a better idea about how it works:
local Widget = require("widget")
local instance = Widget.new({style="bold"})
-- check the values assigned to the instance
print(instance.color) -- blue
print(instance.style) -- bold
-- review of our modified class methods
-- for easy comparison
print(instance.get_color(instance)) -- blue
print(instance.get_style(instance)) -- bold
print(Widget.get_color(instance)) -- blue
print(Widget.get_style(instance)) -- bold
-- equivalent calls to instance methods
print(instance:get_color()) -- blue
print(instance:get_style()) -- bold
We first repeat our modified class methods from the previous example, which required that the instance be passed explicitly to the method. Finally, we called the instance methods to show that they produce the same output, except the instance methods achieve this in a much more concise and ergonomic way. Finally, note that all three types of method calls are valid, and they are all equivalent to each other.
So, now we have covered all of the basics of creating class-like data structures in Lua, and we have shown how we can simulate inheritance to efficiently add functionality to those classes. With just a little bit of practice, these skills represent much of what is required to implement more complex and useful functionality.