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:
- Understand module scripts (Lua)
- Understand metatables and metamethods (particularly the __index metamethod, I do go in-depth of what is happening later in the post, however I highly advise against jumping into this concept blindly) (Lua)
- Understand working with object-oriented programming in Lua (Lua)
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.