Created
November 19, 2012 12:56
'Strict' Structs in Lua.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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