Creating Classes in Lua

Although Lua doesn't strictly have classes, it has features that can be used to emulate class-like behaviors such as instantiation and inheritance. These features are flexible and can be used in various ways, which has led to a wide range of examples on the internet, which has led to some confusion. Our goal is to limit the discussion to a simple pattern that is commonly found in Neovim extensions, and to provide a few variations that demonstrate options for making this pattern work for different sets of requirements.

Motivation

Let's first take a look at the motivation for implementing class-like behavior in Lua. Suppose we are writing a plugin that needs to represent multiple widgets. We start by implementing the widget library, where each widget has two properties, color and style, which hold default values of blue and italic, respectively:

local Widget = {
color = "blue",
style = "italic"
}

return Widget

In this example we want two widgets, so we import the library and create two local variables, one and two. Next, let's print the default values of each widget, then change the value of widget two to 2:

local one = require("widget")
local two = require("widget")

-- check the initial values of each widget
print(one.color) -- blue
print(one.style) -- italic
print(two.color) -- blue
print(two.style) -- italic

-- assign a new style to widget two
two.style = "bold"

-- check the current values of each widget
print(one.color) -- blue
print(one.style) -- bold
print(two.color) -- blue
print(two.style) -- bold

-- print each widget
print(one) -- table: 0x7238b4079f10
print(two) -- table: 0x7238b4079f10

Unfortunately, this didn't behave the way we expected - changing widget two's style property also changed widget one, which is not what we wanted. The problem is that although we assigned widgets to two different local variables, these are not two instances of the widget, these are two references to the same widget! We confirmed this by printing each widget, which shows that both variables reference the same location in memory. As a result, any change made to one widget is made to both widgets.

Instantiation

In order to solve this problem, we need to create multiple instances of our widget, rather than two references to our widget. As we said earlier, there are various ways of doing this in Lua, but we will demonstrate a simple pattern that is suitable for many plugin applications.

First, let's add a constructor function new to our widget:

local Widget = { }

function Widget.new()

local instance = setmetatable(
-- default values go here
{
color = "blue",
style = "italic"
},
-- we will explain this more in the next section
{ __index = Widget }
)

-- can modify the instance here

return instance

end

return Widget

Our constructor function does a few things. First, it creates a local variable instance, which holds the new instance and is eventually returned to the caller. Each instance is created using the setmetatable function, which takes two parameters. Note that we have moved the default color and style properties from the Widget class in the previous example and pass them as the first parameter of setmetatable. Next, the second parameter passed to setmetatable is another table which defines the __index key and assigns our Widget class as its value. The important detail is that the table passed as the first parameter is modified and returned to create the new instance. We will learn a bit more about what setmetatable is doing here in the next section, but for now let's focus on learning how to use this pattern.

Now, let's go back to our plugin and require our widget again as before, except this time we assign the imported value to a single variable, Widget. We are using the convention that capitalized variables, such as Widget, distinguish class definitions from instances of the class.

Note

The main table in our widget library is called Widget, and the local variable assigned to hold a reference to the imported library within the plugin is also called Widget. These variables do not need to have the same names, and either or both could have been simply M or any other name.

Next, we create two instances by calling the constructor function twice and assigning each return value to variables one and two. As before, let's check the initial values, change the style of instance two, then check the values again:

-- import the Widget class
local Widget = require("widget")

-- create two instances of the Widget class
local one = Widget.new()
local two = Widget.new()

-- check the initial values
print(one.color) -- blue
print(one.style) -- italic
print(two.color) -- blue
print(two.style) -- italic

-- change the value of instance `two`
two.style = "bold"

-- check the current values
print(one.color) -- blue
print(one.style) -- italic
print(two.color) -- blue
print(two.style) -- bold

-- print each widget
print(one) -- table: 0x75bd679d05b0
print(two) -- table: 0x75bd679d06b0

Great, as expected we now have two independent instances of our Widget, which we have also confirmed by printing the widgets and confirming that they refer to different locations in memory.

Passing Arguments

Now that we have created a basic Widget class and we have demonstrated that we can create independent instances of that class, we would like to provide a way to initialize each new instance with unique color and style properties. This can be achieved with a simple variation to the constructor:

local Widget = { }

function Widget.new(args)

-- fall back to defaults if args are not provided
args = args or {
color = "blue",
style = "italic"
}

local instance = setmetatable(
args,
{ __index = Widget }
)

-- can modify the instance here

return instance

end

return Widget

The constructor now allows an optional table of arguments to be passed to it with each call to create a new instance, and falls back to the default values if a property is not initialized:

local Widget = require("widget")

local one = Widget.new()
local two = Widget.new({style="bold"})

print(one.style) -- italic
print(two.style) -- bold

Note that the default values are defined inside the constructor, which makes them inaccessible from the outside, allowing them to simulate "private" values. Most plugins support external configuration, so let's add that now.

Configuration

As our final variation, let's add the ability to accept configuration defined in external Lua files, as is common in Neovim plugins. For this example, we define the default values on the main Widget table, then accept an optional config table passed to the constructor. We first ensure that config is not nil, then pass it to the new merge method, which merges external configuration with the default values.

Since Widget and config need to support creation of multiple instances, it is important that neither of these tables are modified. Therefore, the merge function creates a new table that contains all keys from Widget, any values that are present in config, and default values for all others. This table is then passed to setmetatable to create the instance.

local Widget = {
-- default values go here
color = "blue",
style = "italic"
}

function Widget.new(config)

-- allow config to be optional
config = config or {}

-- merge config values with defaults
local values = Widget.merge(config)

-- create the instance
local instance = setmetatable(
values,
{ __index = Widget }
)

-- can modify the instance here

return instance

end

function Widget.merge(config)

-- we don't want to modify either the Widget or config so that
-- we can instantiate multiple, independent instances, so we
-- create a new table to hold the merged values.
local values = {}

-- merge default and config values
-- the result will have all keys from defaults, with any
-- values defined in the config, or else default 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

return Widget

Now, users can define their configuration in a separate file, such as config.lua:

return {
color = "red"
}

Then, users can import their configuration and pass it to the constructor in order to create instances that reflect their configuration and fall back to default values as needed:

local Widget = require("widget")
local config = require("config")

local instance = Widget.new(config)

print(instance.color) -- red
print(instance.style) -- italic

This section focused on a simple pattern that can be used in many plugins, and providing examples of different variations that can be applied to this pattern, which provides the building blocks that can be combined to create more complex structures.

In the next section we will learn a bit more about setmetatable and how it allows us to add methods to our classes.