Skip to content

Instantly share code, notes, and snippets.

@stevedonovan
Created November 19, 2012 12:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stevedonovan/4110502 to your computer and use it in GitHub Desktop.
Save stevedonovan/4110502 to your computer and use it in GitHub Desktop.
'Strict' Structs in Lua.
-- struct.lua
--- defining a struct constructor ---
local function ordered_map (t)
local fields,keys = {},{}
for i,item in ipairs(t) do
local key,value = next(item)
fields[key] = value
keys[i] = key
end
return fields,keys
end
local function map (f,t,...)
local res = {}
for k,v in pairs(t) do
res[k] = f(v,...)
end
return res
end
local struct = {}
local type,getmetatable = type,getmetatable
local function _type (obj)
local t = type(obj)
if t == 'table' then
local s = getmetatable(obj)
if s then
t = s._name or s
end -- otherwise a plain jane table!
end
return t
end
struct.type = _type
local function quote (s) return "'"..s.."'" end
local function _assert_type (s,k,v)
if s._types[k] ~= _type(v) then
error("field "..quote(k).." of "..quote(s._name).." must have type "..quote(s._types[k]),3)
end
end
-- the not-found error
local function _error_nf(tbl,key)
error("field "..quote(key).." is not in "..quote(_type(tbl)),2)
end
local struct_mt = {
_name = 'struct',
-- instances can be created by calling the struct object
__call = function(s,...)
local obj = t or {}
local fields = s._fields
if s._keys and select('#',...) > 1 then -- values by position
obj = {}
local args = {...} -- don't have to deal with nil args!
for i,key in ipairs(s._keys) do
local val = args[i]
if val == nil then break end
obj[key] = args[i]
end
else -- pass it a table (or nothing)
obj = select(1,...) or {}
if getmetatable(obj) == s then -- copy ctor!
local other = obj
obj = {}
for k,v in pairs(other) do
obj[k] = v
end
else
-- attempt to set a non-existent field in ctor?
for k,v in pairs(obj) do
if fields[k]==nil then
_error_nf(setmetatable(obj,s),k)
else
_assert_type(s,k,v)
end
end
end
end
-- fill in any default values if not supplied
for k,v in pairs(fields) do
if obj[k]==nil then
obj[k] = v
end
end
s._count = s._count + 1
obj._count = s._count
if s._proxied then
obj = { _proxy = obj }
end
setmetatable(obj,s)
return obj
end;
}
local append = table.insert
local function _tostring (s)
return function(sval)
local res = {}
for k in pairs(s._fields) do
append(res,k..'='..tostring(sval[k]))
end
res = table.concat(res,',')
return s._name..' #'..sval._count..':'..res
end
end
local function _eq (v1,v2)
local M1,M2 = getmetatable(v1),getmetatable(v2)
if M1 ~= M2 then return false end
for k in pairs(M1._fields) do
if v1[k] ~= v2[k] then return false end
end
return true
end
local function proxy_index (t,key)
local val = t._proxy[key]
if not val then _error_nf(t,key) end
return val
end
-- creating a new struct triggered by struct.STRUCTNAME
setmetatable(struct,{
__index = function(tbl,sname)
-- so we create a new struct object with a name
local s = {_name = sname}
-- and put the struct in the enclosing context
_G[sname] = s
s._count = 0
-- reading or writing an undefined field of this struct is an error
s.__tostring = _tostring(s)
s.__eq = _eq
-- the struct has a ctor
setmetatable(s,struct_mt)
-- return a function that sets the struct's fields
return function(t)
local fields
if not t._proxy then
s.__index = _error_nf
s.__newindex = _error_nf
else
s._proxied = true
t._proxy = nil
s.__index = proxy_index
s.__newindex= function(t,key,value)
if fields[key] == nil then _error_nf(t,key) end
_assert_type(s,key,value)
t._proxy[key] = value
end
end
if type(t[1]) == 'table' then
fields,s._keys = ordered_map(t)
else
fields = t
end
s._fields = fields
s._types = map(_type,fields)
return s
end
end
})
return struct
struct = require 'struct'
local function throws (msg,fun)
local ok,err = pcall(fun)
if not ok==false or not err:match(msg) then
error("assert throws "..msg,2)
end
end
local function asserts (v1,v2)
if not tostring(v1) == tostring(v2) then
error("asserts "..v1..'~='..v2,2)
end
end
struct.Point {
x = 0,
y = 0
}
local p = Point{x=100,y=200}
-- reading or writing an unknown key raises an error
throws("field 'X' is not in 'Point'",function()
p.X = 300
end)
throws("field 'X' is not in 'Point'",function()
print(p.X)
end)
-- structs have a default string representation
asserts(p,'Point #1:y=200,x=100')
struct.Record {
A = 'text',
B = 0,
C = false,
D = Point()
}
-- using defaults
r = Record{A = 'text'}
-- any object defaults remain references to the original default
asserts(r,"Record #1:A=text,D=Point #2:y=0,x=0,C=false,B=0")
-- we catch type errors in constructors
throws("field 'C' of 'Record' must have type 'boolean'",function()
r = Record{A = 'text',C=1}
end)
-- alternatively, struct specification can have ordered keys.
-- Only then can you have values with implied keys!
local Person = struct.Person {
{name = 'text'},
{age = 21},
}
-- at least one argument is needed, otherwise it goes back to the
-- key-value method!
local P = Person("Alice",16)
asserts(P,'Person #1:name=Alice,age=16')
-- copy ctor
P = Person(P)
asserts(P,'Person #2:name=Alice,age=16')
-- pick up a default value
P = Person{name='Bob'}
asserts(P,'Person #3:name=Bob,age=21')
-- not the same object, but __eq is defined appropriately
assert(Person("Fred",20) == Person("Fred",20))
-- we expect an error when setting a wrong type in a ctor...
throws("field 'age' of 'Person' must have type 'number'",function()
p = Person{name="Frank",age='?'}
end)
-- using a proxied struct means we can catch type errors on assignment
struct.SPerson { name = 'text', age = 21, _proxy=true }
sp = SPerson{name='bob'}
throws("field 'age' of 'SPerson' must have type 'number'",function()
sp.age = '?'
end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment