Method chaining in Lua OOP

Introduction

Hi,

I’ve recently been learning JavaScript (JS) and there is an interesting practice in JS and other languages’ object-oriented programming (OOP) called method chaining. It is a neat and visually appealing way to organize mass call object methods that is not often seen in Lua if at all. From Wikipedia, “method chaining eliminates an extra variable for each intermediate step. The developer is saved from the cognitive burden of naming the variable and keeping the variable in mind.”

Prerequisites

This is a more so advanced tutorial and doesn’t really apply to you unless you’re looking for organization techniques with custom objects in Lua. If you don’t understand one of them, click on the prerequisite and it’ll bring you to a tutorial/introduction on it. If you’re looking for a tutorial that applies to Lua in general (not just Roblox lua), click the (Lua) button.

You must:

Okay, but what is method chaining?

Method chaining is a practice in object-oriented programming that allows mass calling many methods of a single object, well, in a chain, or in a row.

Let me explain, in JavaScript, we can construct an object by doing var newObject = new Object();, in Lua, the typical constructor is local object = object.new(), this would be the equivalent. However, in many instances of coding in JavaScript (particularly Discord.js which was where I learned this practice), we can also assign properties by calling methods in JS’ objects like so:

var newObject = new Object()
    .setColour(255, 255, 255)
    .setTransparency(0.5)
    .setMaterial('Neon');

Said code would call all three methods on the same object, and newObject would be this constructed object.

Going on a tangent here, but if you use string:method() to call string methods instead of string.method(‘string’), or if you’re using string.method(string):method(), you may be using method chaining.

For example, if we do this:

local s = 'cool string'
s:upper():sub(5,10)

That is an example of method chaining to an extent, however it may not be apparent since strings are primitive values with metatables, not objects.

If you want to expand on your method chaining knowledge, see the Wikipedia article on method chaining.

The actual tutorial

Anyway, let me get back on-topic. Assuming you’re familiar with Lua objects, this practice is not common, the most common objects with methods are constructed in a manner like so:

local object = {}
object.__index = object

function object:Method(arg)
    self.property = arg
end
function object.new()
    return setmetatable({}, object)
end

And if we call object:Method(), it doesn’t return anything so we cannot manipulate it further. However, if we make a small tweak and return self from object:Method() or any other methods, method chaining becomes possible.

local object = {}
object.__index = object
function object:SetProperty(propertyName, propertyValue)
    self[propertyName] = propertyValue
    return self
end

function object.new()
    return setmetatable({}, object)
end

So, now we have implemented a basic method that supports method chaining:

local coolObject = object.new()
	:SetProperty('name', 'bob')
	:SetProperty('superCool', true)
	:SetProperty('bestObjectEver', true)

coolObject will be equal to the constructed object and will have three properties, “name”, “superCool”, and “bestObjectEver”, whose values are “bob”, true and true respectively.

So, although this is cool and provides a visual aid, I should explain what is going on here so let me break it down. I’ve created a (poorly drawn) flowchart to explain what happens when we call a method, and why object chaining works:

Use cases

While method chaining provides a visual aid, it can also help when making arrays of objects and working with many objects. For example, let’s say we need to create an object inside of a table, and we need to call one of this object’s methods. Traditionally, it would look something like this:

local myObjects = {
    object.new()
}
myObjects[1]:Method()
myObjects[2]:Method2()

We have to index the myObjects table two times and call two separate methods which can be tedious when we’re working with many different objects, or with many similar objects that require arbitrary arguments to be passed to the method where looping is not efficient and/or practical. Using method chaining, we can avoid indexing the table every single time:

local myObjects = {
    object.new()
        :Method(true)
        :Method2();
    object.new()
        object:Method3('arg')
        object:Method(5);
}

Both code samples do the same thing, however the method chaining option looks much more legible.

Caveats

Final part, I want to be totally transparent with you, there are a few things you should know with respect to method chaining. Although it does provide a visual aid with objects and whatnot, a few things are important to know:

1- Method chaining does not work with assigning values outside of a method.
For example,

local obj = object.new()
    .property = true
    :SetProperty('test', true)

That code sample will error with ‘Expected identifier when parsing expression, got ‘=’’

2- Method chaining is very slightly slower. My (probably flawed) findings show that creating 1,000 objects and calling :SetProperty() on them each 3 times takes, on average, between 0.00001 and 0.00004 seconds longer with method chaining. Obviously, this will fluctuate depending on what attempts you compare but an average number is a better number to look at because it shows the cumulative attempts as opposed to individual attempts, I’m going off topic though, this isn’t a statistics lesson.

Testing code

Note: If you’re working with any fork of Lua other than Luau (Roblox Lua), this code won’t compile because I’m using the += compound assignment operator. You should replace these with var = var + number for it to compile elsewhere.

local object = {}
object.__index = object
function object:SetProperty(propertyName, propertyValue)
	self[propertyName] = propertyValue
	return self
end

function object.new()
	return setmetatable({}, object)
end

local t = {}

for i = 1,10 do
	local init = os.clock()
	for i = 1,1000 do
		local coolObject = object.new()
			:SetProperty('s', true)
			:SetProperty('ss', true)
			:SetProperty('sss', true)
	end
	local timeTook = os.clock() - init
	print('Attempt', i, ' (with method chaining): 1000x took about', timeTook)
	table.insert(t, timeTook)
	task.wait(0.1) -- let it cool down for a moment
end

local avg = 0
for i,v in ipairs(t) do
	avg += v
end

print('Average for 1,000 assignments: about', avg / 10)

task.wait(5) -- let it cool down for a moment (just in case)

local object = {}
object.__index = object
function object:SetProperty(propertyName, propertyValue)
	self[propertyName] = propertyValue
end

function object.new()
	return setmetatable({}, object)
end

local t = {}

for i = 1,10 do
	local init = os.clock()
	for i = 1,1000 do
		local coolObject = object.new()
		coolObject:SetProperty('s', true)
		coolObject:SetProperty('ss', true)
		coolObject:SetProperty('sss', true)
	end
	local timeTook = os.clock() - init
	print('Attempt', i, ' (without method chaining): 1000x took about', timeTook)
	table.insert(t, timeTook)
	task.wait(0.1) -- let it cool down for a moment
end

local avg = 0
for i,v in ipairs(t) do
	avg += v
end

print('Average for 1,000 assignments (without method chaining): about', avg / 10)


3- Method chaining does not work with Roblox datatypes.
4- Method chaining does not work with methods that require arguments from the same method chain:

local object = object.new()
    :GetValue() -- let's say :GetValue returns a number
    :GetValueIsEqual() -- let's say :GetValueIsEqual requires a value returned from the GetValue function from above, this will not work since we can't pass parameters returned from other method-chained functions.
21 Likes

Cool community tutorial, it has a pretty good explanation of method chaining (and OOP in general). Nice job!

3 Likes