Methods in Lua

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".

Note

"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:

  1. While class methods are accessed using a ., instance methods are accessed using :.
  2. Instance methods implicitly pass the self variable 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.