5.9 Integrating Tables and Functions

Tables and first class functions allow us to further abstract related components of a script into more complex building blocks.

Say we wanted to create a shop vendor who sells some items.

A crude way to do this is a to create variables to store the vendor’s inventory stock and price.

Then for purchasing duty, since we don’t want to rewrite the same code over again, we will create a function that handles that for us.

Lua
local giantBeetleStock = 2
local giantBeetlePrice = 10

local function buyGiantBeetle(gold)
	if giantBeetleStock > 0 then
		if gold >= giantBeetlePrice then
			giantBeetleStock = giantBeetleStock - 1
			print("Thanks for the purchase!")
			return "giant beetle"
		else
			print("Not enough gold!")
			return false, gold
		end
	end
	print("giant beetle out of stock")
end

local result = buyGiantBeetle(5) --Output: Not enough gold!
result = buyGiantBeetle(10) --Output: Thanks for the purchase!
result = buyGiantBeetle(10) --Output: Thanks for the purchase!
result = buyGiantBeetle(10) --giant beetle out of stock

However, this has a few shortcomings.

First, as the merchant’s shop expands, we would have to implement this code for every type of item that gets added to the inventory. If there are dozens or hundreds of possible vendor items, this is not practical.

Second, the scope for stock and price variables can be accessed and modified from anywhere in the script. For a larger, more complex script this makes maintenance difficult and a nightmare if you have to chase bugs.

Instead, we can utilize a table to group all of the relevant components together.

Lua
local alchemyVendor = {}

alchemyVendor["inventory"] = {
	["giant beetle"] = {["price"] = 10, ["stock"] = 2},
	["wooden stick"] = {["price"] = 5, ["stock"] = 6},
}

alchemyVendor["buy item"] = function(item, gold)
	local inventory = alchemyVendor["inventory"]
	if not inventory[item] or inventory[item]["stock"] <= 0 then
		print(item, "out of stock") return
	end

	if gold < inventory[item]["price"] then print("Not enough gold!") return false, gold end

	print("Thanks for the purchase!")
	inventory[item]["stock"] = inventory[item]["stock"] - 1

	return item
end

local result = alchemyVendor["buy item"]("giant beetle", 5) --Output: Not enough gold!
result = alchemyVendor["buy item"]("giant beetle", 10) --Output: Thanks for the purchase!
result = alchemyVendor["buy item"]("wooden stick", 5) --Output: Thanks for the purchase!
result = alchemyVendor["buy item"]("giant beetle", 10) --Output: Thanks for the purchase!
result = alchemyVendor["buy item"]("giant beetle", 10) --giant beetle out of stock

Now the merchant has his own inventory kept inside a table that can store any number of items and along with the proper functionality to handle shop duty.


While much better, this still has some problems.

The first is that the function references the variable alchemyVendor inside itself.

And tables get reassigned often, meaning if this variable is reassigned but the function depends on this specific variable identifier, the code will likely break.

Lua
...
...
local result = alchemyVendor["buy item"]("giant beetle", 5) --Output: Not enough gold!
result = alchemyVendor["buy item"]("giant beetle", 10) --Output: Thanks for the purchase!

local reassigedVendor = alchemyVendor 
alchemyVendor = {} -- vendor is now unusable

result = reassigedVendor["buy item"]("giant beetle", 10) --Error: attempt to index nil with 'inventory'
result = reassigedVendor["buy item"]("giant beetle", 10) --Error
...
...

Second, although the merchant’s inventory will only be modified by the provided functions, the inventory is still visible throughout the script and can still be inadvertently modified elsewhere.


Fortunately, we do have a way to hide away tables and variables and make them only accessible in some scope. We’ve met them before.

Lexical scope and closures.

Lua
local alchemyVendor

do
	local temporaryTable = {}

	local inventory = { --upvalue 
		["giant beetle"] = {["price"] = 10, ["stock"] = 2},
		["wooden stick"] = {["price"] = 5, ["stock"] = 6},
	}

	temporaryTable["buy item"] = function (item, gold)
		if not inventory[item] or inventory[item]["stock"] <= 0 then
			print(item, "out of stock") return
		end

		if gold < inventory[item]["price"] then print("Not enough gold!") return false, gold end

		print("Thanks for the purchase!")
		inventory[item]["stock"] = inventory[item]["stock"] - 1

		return item
	end

	alchemyVendor = temporaryTable
end

local result = alchemyVendor["buy item"]("giant beetle", 5) --Output: Not enough gold!
result = alchemyVendor["buy item"]("giant beetle", 10) --Output: Thanks for the purchase!

local reassignedVendor = alchemyVendor 
alchemyVendor = nil

result = reassignedVendor["buy item"]("giant beetle", 10) --Output: Thanks for the purchase!

local buyFunction = reassignedVendor["buy item"]
local buyGiantBeetle = function(gold) buyFunction("giant beetle", gold) end

result = buyGiantBeetle(10) --Output: giant beetle out of stock

The vendor’s inventory is now hidden from the rest of the script and we provide only an interface for other scripts to work with.

And just like that, you my friend accidentally stepped into a pile of…