Metatable logic confusion

I am very new with metatables so please bear with me. Why is the line “Object.__index = Object”
placed where it is? From my understanding, the __index method fires when a metatable is indexed. In the provided example script, how is the metatable being indexed? Any help would be greatly appreciated!

local Object = {} --Constructor
Object.ClassName = "Object"
Object.__index = Object

function Object:GetName()
	return self.Name
end

return {
	new = function(name)
		local newObject = {
			Name = name or Object.ClassName
		}
		return setmetatable(newObject, Object)
	end
}

this video might help you understand __index

Close, __index fires when the table in question doesn’t already have what’s wanted. You can think of it as a backup.

In this case, if newObject doesn’t have say .ClassName, it will default to "Object". The __index method is just telling whatever table it’s being linked to to use what it has, which is why it’s setting itself as the metamethod.

1 Like

This is a pretty common structure used in Lua to simulate object-oriented-programming.

local Dog = {}

function Dog:Bark()
  print(self.name .. " barked!")
end

function Dog.new(name)
  local self = setmetatable({}, {__index = Dog})
  self.name = name
  return self
end

local buddy = Dog.new("Buddy")
local skippy = Dog.new("Skippy")

buddy:Bark() --> Buddy barked!
skippy:Bark() --> Skippy barked!

The __index metamethod tells a table what to do when it is indexed and the value is nil. It can be either a table or a function. This is useful because it allows you to create a “fallback” table that will return values at indices that are not in the “main” table. This allows you to to create a bunch of different objects that all have different values, yet share the same functions.

Here’s an simpler example of how __index can be used:

local tableA = {a = 42}
local tableB = {b = 100}
setmetatable(tableB, {__index = tableA})

-- first checks if index "b" is in tableB.  it is!  so 100 is returned
print(tableB.b) --> 100

-- first checks if index "a" is in tableB.  it isn't.  instead of returning nil,
-- it will first check if tableB has a metatable with a __index metamethod.
-- it does.  so, it will check to see if "a" is in tableA and return 42
print(tableB.a) --> 42

-- in this case, we fall through both the main table & the metatable to return nil:
print(tableB.c) --> nil

You can also use a function for __index, which will be called when a table index is nil:

local tab = {a = 42}
setmetatable(tab, {
  __index = function(self, key)
    return string.format("The key '%s' is not in that table!", tostring(key))
  end
})

print(tab.a) --> 42
print(tab.b) --> The key 'b' is not in that table!

Edit:
Lua also has functions rawget and rawset that allow you to get/set a value from a table without invoking any metamethods:

print(rawget(tab, "a")) --> 42
print(rawget(tab, "b")) --> nil (__index is not invoked and nil is returned)
4 Likes

Thank you for the explanation, it really helped me understand!

now that we live in the modern world and have types, I like to design things like this for great autocomplete & typechecking:

--!strict

local module = {}

type raw_schema = { 
	_field: number,
	-- ... more values
}

type prototype_schema = { 
	SetField: (self: schema, field: number) -> (),
	GetField: (self: schema) -> number,
	-- ... more methods
}

export type schema = typeof(setmetatable({}::raw_schema, {__index = {}::prototype_schema}))

local prototype = {}

function prototype.GetField(self: schema): number 
	return self._field
end

function prototype.SetField(self: schema, field: number) 
	self._field = field
end

function module.New(field: number): schema
	local selfRaw: raw_schema = {
		_field = field,
		-- ...
	}
	local self: schema = setmetatable(selfRaw, {__index = prototype})

	return self
end

return module
local MyModule = require(game.ServerScriptService.MyModule)

local object = MyModule.New(42)
object:SetField(100)
print(`field: {object:GetField()}`) -- 100
1 Like