Lua Metatables

Metatable Basics

Setting and using metatables
local t = {}
local mt = {}
setmetatable(t, mt)

-- Check metatable
print(getmetatable(t) == mt)    -- true

-- Shorthand: set in constructor
local t2 = setmetatable({}, mt)

Metamethods

__index — property lookup fallback
-- __index as table (prototype chain)
local defaults = {
  timeout = 30,
  retries = 3,
  port    = 443,
}

local config = setmetatable({
  port = 8080,      -- override
}, { __index = defaults })

print(config.port)       -- 8080 (found in config)
print(config.timeout)    -- 30 (falls through to defaults)
print(config.retries)    -- 3 (falls through to defaults)

-- __index as function (computed lookup)
local env = setmetatable({}, {
  __index = function(self, key)
    return os.getenv(key:upper())
  end,
})
print(env.home)           -- /home/evanusmodestus (reads $HOME)
__newindex — intercept writes
local readonly = setmetatable({}, {
  __newindex = function(self, key, value)
    error("attempt to write to readonly table: " .. key)
  end,
})
-- readonly.x = 1          -- error!

-- Logging proxy
local tracked = setmetatable({}, {
  __newindex = function(self, key, value)
    print(string.format("SET %s = %s", key, tostring(value)))
    rawset(self, key, value)         -- bypass metamethod
  end,
})
tracked.hostname = "vault-01"        -- prints: SET hostname = vault-01

Operator Overloading

Arithmetic and comparison metamethods
local Vec2 = {}
Vec2.__index = Vec2

function Vec2.new(x, y)
  return setmetatable({ x = x, y = y }, Vec2)
end

function Vec2:__add(other)
  return Vec2.new(self.x + other.x, self.y + other.y)
end

function Vec2:__eq(other)
  return self.x == other.x and self.y == other.y
end

function Vec2:__tostring()
  return string.format("(%d, %d)", self.x, self.y)
end

function Vec2:__len()
  return math.sqrt(self.x ^ 2 + self.y ^ 2)
end

local a = Vec2.new(1, 2)
local b = Vec2.new(3, 4)
local c = a + b                      -- Vec2(4, 6) via __add
print(tostring(c))                   -- "(4, 6)" via __tostring
print(#a)                            -- 2.236 via __len

-- Available metamethods:
-- __add(+) __sub(-) __mul(*) __div(/) __mod(%) __pow(^)
-- __unm(unary -) __concat(..) __len(#)
-- __eq(==) __lt(<) __le(<=)
-- __call() __tostring()

OOP with Metatables

Class pattern
local Device = {}
Device.__index = Device

function Device.new(hostname, ip)
  return setmetatable({
    hostname = hostname,
    ip = ip,
  }, Device)
end

function Device:ping()
  return os.execute("ping -c 1 -W 2 " .. self.ip .. " > /dev/null 2>&1")
end

function Device:__tostring()
  return self.hostname .. " (" .. self.ip .. ")"
end

-- Inheritance
local Switch = setmetatable({}, { __index = Device })
Switch.__index = Switch

function Switch.new(hostname, ip, vlans)
  local self = Device.new(hostname, ip)
  self.vlans = vlans or {}
  return setmetatable(self, Switch)
end

function Switch:vlan_count()
  return #self.vlans
end

local sw = Switch.new("sw-core-01", "10.50.1.2", { 10, 20, 30 })
print(sw:vlan_count())       -- 3
print(tostring(sw))          -- sw-core-01 (10.50.1.2) -- inherited