You’re probably used to passing values to functions, which allow you to specify What you want to operate on. In Lua, and indeed most languages, you can also pass functions as arguments which let you specify How something is operated on. Armed with this you can abstract your code to a much higher degree and keep your statements tighter, containing more relevant information per statement. A common example of this is finding the maximum element of a table. Here is a typical way of doing this:
local oldestIndex = 0
local oldestTime = 0
local oldestEntry = nil
for i, oldItemEntry in spawnedItems do
if oldestEntry == nil or oldItemEntry.spawnedAtTime < oldestTime then
oldestTime = oldItemEntry.spawnedAtTime
oldestEntry = oldItemEntry
oldestIndex = i
end
end
This finds the item in spawnedItems whose spawnedAtTime was the longest ago. If we wanted to do the same check but instead find the most recently spawned item, or use a different member for comparison, we would have to repeat most of this code. We can avoid this by abstracting the comparison step and looping step as their own functions:
local function GreaterThan(a, b)
return a - b
end
local function LessThan(a, b)
return b - a
end
local function MaxSpawnedAtTime(t, comparator)
local maxValue = nil
local maxIndex = nil
for i, v in pairs(t) do
if maxValue == nil or comparator(v.spawnedAtTime, t[maxIndex].spawnedAtTime) > 0 then
maxValue = v.spawnedAtTime
maxIndex = i
end
end
return maxIndex, maxValue
end
maxi, maxv = MaxSpawnedAtTime(spawnedItems, GreaterThan)
In this example, the function comparator passed to MaxSpawnedAtTime determines the order that the two items being compared are subtracted. When GreaterThan is used, the comparison is:
v.spawnedAtTime - t[maxIndex].spawnedAtTime > 0
which is equivalent to
v.spawnedAtTime > t[maxIndex].spawnedAtTime
When LessThan is used as the comparator, the comparison is reversed. This type of ‘subtraction’ comparator pattern is a classic example used in C’s strcmp() and sort(). Lua’s table.sort() also accepts this exact kind of comparator. We can do one better by providing a ‘selector’ function that determines the value chosen for selection. We could just provide a string, such as “spawnedAtTime”, to select this, but using a function allows us to make our comparison much more versatile, as we will see later:
local function SpawnedAtTime(e)
return e.spawnedAtTime
end
local function Max(t, selector, comparator)
local maxValue = nil
local maxIndex = nil
for i, v in pairs(t) do
if maxValue == nil or comparator(selector(v), selector(t[maxIndex])) > 0 then
maxValue = selector(v)
maxIndex = i
end
end
return maxIndex, maxValue
end
maxi, maxv = Max(spawnedItems, SpawnedAtTime, GreaterThan)
Our selector can be very advanced. For example, it could contain a distance check, letting us choose the closest item instead of the oldest:
local function DistanceFromButton(e)
return (e.instance.PrimaryPart.Position - button.Position).Magnitude
end
local function Max(t, selector, comparator)
local maxValue = nil
local maxIndex = nil
for i, v in pairs(t) do
if maxValue == nil or comparator(selector(v), selector(t[maxIndex])) > 0 then
maxValue = selector(v)
maxIndex = i
end
end
return maxIndex, maxValue
end
maxi, maxv = Max(spawnedItems, DistanceFromButton, GreaterThan)
Our function is now called such that it returns the element in the table whose instance (an Instance in the workspace) is furthest from the button. Note that we assume button has been defined previously. However, we can make this more powerful using anonymous functions. Anon functions can capture variables in their parent scope. In that case, button only needs to be defined right before the call to Max with an anonymous function as the selector.
Our decision to use this functional pattern has some apparent tradeoffs. The first disadvantage is that the Max function is harder to understand initially, especially if you did not have an example usage. If you choose to use this pattern, make sure to adequately explain what your function parameters need to do in a comment above the Max function.
The advantage of this pattern is we now have a high level of clarity whenever we call Max. Reading our code we can read the following line:
Max(spawnedItems, DistanceFromButton, GreaterThan)
as simply “Find the item in spawnedItems whose DistanceFromButton is Greatest”
This code has good self-documenting properties as opposed to original example, which requires careful reading of several lines and cannot be adapted to different tasks. Our finished code thus greatly reduces code re-use.
Here is the full example, as I have used it in some respawning code:
local function SpawnedAtTime(e)
return e.spawnedAtTime
end
local function DistanceFromButton(e)
local modelPrimaryPart = e.instance.PrimaryPart
local distance = 0
if modelPrimaryPart ~= nil then
distance = (modelPrimaryPart.Position - button.Position).Magnitude
else
warn("Spawned model does not have a PrimaryPart set, can't calculate distance from spawn button!")
end
return distance
end
local function Max(t, selector, comparator)
local maxValue = nil
local maxIndex = nil
for i, v in pairs(t) do
if maxValue == nil or comparator(selector(v), selector(t[maxIndex])) > 0 then
maxValue = selector(v)
maxIndex = i
end
end
return maxIndex, maxValue
end
--Remove oldest spawned item
local function RemoveOldestSpawn()
local oldestIndex = Max(spawnedItems, SpawnedAtTime, function(a, b) return b - a end)
RemoveSpawn(oldestIndex)
end
--Remove furthest spawned item from button
local function RemoveFurthestSpawn()
local furthestIndex = Max(spawnedItems, DistanceFromButton, function(a, b) return a - b end)
RemoveSpawn(furthestIndex)
end
A note about DRY, or why we want to reduce code repetition:
If you had duplicated the original code and discovered a bug in one instance of it, you now have to worry if the other copy of the same code has the same bug. The small variations between the copies might make it very difficult to tell for sure. The problem only gets worse the more repetitions there are. When you keep code non-repeating, not only will you avoid this scenario, but the same bug will probably be discovered earlier, and only needs to be ironed out in one place. Additionally, writing code this way gives you more places to verify assumptions, since it is clear what is going into each call to Max and what should come back out.