BREAKING class interface refactor - all classes will need a minor update, see class.lua

tl;dr is that new no longer needs to call init, calling :new() directly in user code is not allowed, properties are copied, metamethods work, and a config table is needed rather than a class to extend from, so use {extends = superclass} if you want a minimal fix
This commit is contained in:
Max Cahill 2021-07-15 16:09:08 +10:00
parent e1bb76d419
commit 3cc177a0c0
12 changed files with 255 additions and 251 deletions

View File

@ -18,13 +18,13 @@
local path = (...):gsub("async", "") local path = (...):gsub("async", "")
local class = require(path .. "class") local class = require(path .. "class")
local async = class() local async = class({
name = "async",
})
function async:new() function async:new()
return self:init({ self.tasks = {}
tasks = {}, self.tasks_stalled = {}
tasks_stalled = {},
})
end end
--add a task to the kernel --add a task to the kernel
@ -138,4 +138,4 @@ function async:add_interval(f, delay)
end) end)
end end
return async return async

176
class.lua
View File

@ -1,83 +1,131 @@
--[[ --[[
barebones oop basics barebones oop basics
supports basic inheritance and means you don't have to build/set your own metatable each time
todo: collect some stats on classes/optional global class registry call the class object to construct a new instance
classes are used as metatables directly so that
metamethods "just work" - except for index, which is
used to hook up instance methods
classes do use a prototype chain for inheritance, but
also copy their interfaces (including superclass)
we copy interfaces in classes rather than relying on
a prototype chain, so that pairs on the class gets
all the methods when implemented as an interface
class properties are not copied and should likely
be accessed through the concrete class object so
that everything refers to the same object
arguments (all optional):
name (string):
the name to use for type()
extends (class):
superclass for basic inheritance
implements (ordered table of classes):
mixins/interfaces
default_tostring (boolean):
whether or not to provide a default tostring function
]] ]]
local function class(inherits) --generate unique increasing class ids
local c = {} local class_id_gen = 0
--class metatable local function next_class_id()
setmetatable(c, { class_id_gen = class_id_gen + 1
--wire up call as ctor return class_id_gen
__call = function(self, ...) end
return self:new(...)
end,
--handle single inheritence chain
__index = inherits,
})
--instance metatable
c.__mt = {
__index = c,
}
--common class functions
--internal initialisation --implement an interface into c
--sets up an initialised object with a default value table local function implement(c, interface)
--performing a super construction if necessary, and (re-)assigning the right metatable c.__is[interface] = true
function c:init(t, ...) for k, v in pairs(interface) do
if inherits and inherits.new then if c[k] == nil and type(v) == "function" then
--construct superclass instance, then overlay args table c[k] = v
local ct = inherits:new(...)
for k,v in pairs(t) do
ct[k] = v
end
t = ct
end end
--upgrade to this class and return end
return setmetatable(t, self.__mt) end
--build a new class
local function class(config)
local class_id = next_class_id()
config = config or {}
local extends = config.extends
local implements = config.implements
local name = config.name or ("unnamed class %d)"):format(class_id)
local c = {}
--unique generated id per-class
c.__id = class_id
--the class name
c.__name = name
--prototype
c.__index = c
--return the name of the class
function c:type()
return name
end end
--constructor if config.default_tostring then
--generally to be overridden function c:__tostring()
function c:new() return name
return self:init({}) end
end
--class metatable to set up constructor call
setmetatable(c, {
__call = function(self, ...)
local instance = setmetatable({}, self)
instance:new(...)
return instance
end,
__index = extends,
})
--checking class membership for probably-too-dynamic code
--returns true for both extended classes and implemented interfaces
--(implemented with a hashset for fast lookups)
c.__is = {}
c.__is[c] = true
function c:is(t)
return self.__is[t] == true
end end
--get the inherited class for super calls if/as needed --get the inherited class for super calls if/as needed
--allows overrides that still refer to superclass behaviour --allows overrides that still refer to superclass behaviour
function c:super() c.__super = extends
return inherits --nop by default
function c:super() end
if c.__super then
--perform a super construction for an instance
function c:super(...)
c.__super.new(self, ...)
end
--implement superclass interface
implement(c, c.__super)
end end
--delegate a call to the superclass, by name
--still a bit clumsy but much cleaner than the inline equivalent, --implement all the passed interfaces/mixins
--plus handles heirarchical complications, and detects various mistakes --in order provided
function c:super_call(func_name, ...) if implements then
-- for _, interface in ipairs(implements) do
if type(func_name) ~= "string" then implement(c, interface)
error("super_call requires a string function name to look up, got "..tostring(func_name))
end end
--todo: memoize the below :) end
local previous_impl = c:super()
--find the first superclass that actually has the method --default constructor, just proxy to the super constructor
while previous_impl and not rawget(previous_impl, func_name) do --override it and use to set up the properties of the instance
previous_impl = previous_impl:super() --but don't forget to call the super constructor!
end function c:new(...)
if not previous_impl then self:super(...)
error("failed super call - no superclass in the chain has an implementation of "..func_name)
end
-- get the function
local f = previous_impl[func_name]
if not f then -- this should never happen because we bail out earlier
error("failed super call - missing function "..func_name.." in superclass")
end
-- check if someone reuses that reference
if f == self[func_name] then
error("failed super call - function "..func_name.." is same in superclass as in derived; this will be a infinite recursion!")
end
-- call that function
return f(self, ...)
end end
--done --done

View File

@ -39,6 +39,7 @@ local _batteries = {
manual_gc = require_relative("manual_gc"), manual_gc = require_relative("manual_gc"),
colour = require_relative("colour"), colour = require_relative("colour"),
pretty = require_relative("pretty"), pretty = require_relative("pretty"),
make_pooled = require_relative("make_pooled"),
} }
--assign aliases --assign aliases

43
make_pooled.lua Normal file
View File

@ -0,0 +1,43 @@
--[[
add pooling functionality to a class
adds a handful of class and instance methods
]]
return function(class, limit)
--shared pooled storage
local _pool = {}
--size limit for tuning memory upper bound
local _pool_limit = limit or 128
--flush the entire pool
function class:flush_pool()
if #_pool > 0 then
_pool = {}
end
end
--drain one element from the pool, if it exists
function class:drain_pool()
if #_pool > 0 then
return table.remove(_pool)
end
return nil
end
--get a pooled object
--(re-initialised with new, or freshly constructed if the pool was empty)
function class:pooled(...)
if #_pool == 0 then
return c(...)
end
return c.drain_pool():new(...)
end
--release a vector to the pool
function class:release()
if #_pool < _pool_limit then
table.insert(_pool, self)
end
end
end

View File

@ -4,13 +4,13 @@
local path = (...):gsub("pubsub", "") local path = (...):gsub("pubsub", "")
local class = require(path .. "class") local class = require(path .. "class")
local pubsub = class() local pubsub = class({
name = "pubsub",
})
--create a new pubsub bus --create a new pubsub bus
function pubsub:new() function pubsub:new()
return self:init({ self.subscriptions = {}
subscriptions = {},
})
end end
--(internal; notify a callback set of an event) --(internal; notify a callback set of an event)

View File

@ -66,7 +66,7 @@ Extensions to existing lua core modules to provide missing features.
General utility data structures and algorithms to speed you along your way. General utility data structures and algorithms to speed you along your way.
- [`class`](./class.lua) - Single-inheritance oo in a single function. - [`class`](./class.lua) - OOP with inheritance and interfaces in a single function.
- [`functional`](./functional.lua) - Functional programming facilities. `map`, `reduce`, `any`, `match`, `minmax`, `mean`... - [`functional`](./functional.lua) - Functional programming facilities. `map`, `reduce`, `any`, `match`, `minmax`, `mean`...
- [`sequence`](./sequence.lua) - An oo wrapper on sequential tables, so you can do `t:insert(i, v)` instead of `table.insert(t, i, v)`. Also supports method chaining for the `functional` interface above, which can save a lot of needless typing! - [`sequence`](./sequence.lua) - An oo wrapper on sequential tables, so you can do `t:insert(i, v)` instead of `table.insert(t, i, v)`. Also supports method chaining for the `functional` interface above, which can save a lot of needless typing!
- [`set`](./set.lua) - A set type supporting a full suite of set operations with fast membership testing and `ipairs`-style iteration. - [`set`](./set.lua) - A set type supporting a full suite of set operations with fast membership testing and `ipairs`-style iteration.

View File

@ -6,12 +6,19 @@
]] ]]
local path = (...):gsub("sequence", "") local path = (...):gsub("sequence", "")
local class = require(path .. "class")
local table = require(path .. "tablex") --shadow global table module local table = require(path .. "tablex") --shadow global table module
local functional = require(path .. "functional") local functional = require(path .. "functional")
local stable_sort = require(path .. "sort").stable_sort local stable_sort = require(path .. "sort").stable_sort
local sequence = class(table) --proxy missing table fns to tablex api --(not a class, because we want to be able to upgrade tables that are passed in without a copy)
local sequence = {}
sequence.__index = sequence
setmetatable(sequence, {
__index = table,
__call = function(self, ...)
return sequence:new(...)
end,
})
--iterators as method calls --iterators as method calls
--(no pairs, sequences are ordered) --(no pairs, sequences are ordered)
@ -21,7 +28,7 @@ sequence.iterate = ipairs
--upgrade a table into a sequence, or create a new sequence --upgrade a table into a sequence, or create a new sequence
function sequence:new(t) function sequence:new(t)
return self:init(t or {}) return setmetatable(t or {}, sequence)
end end
--sorting default to stable --sorting default to stable

17
set.lua
View File

@ -6,21 +6,20 @@ local path = (...):gsub("set", "")
local class = require(path .. "class") local class = require(path .. "class")
local table = require(path .. "tablex") --shadow global table module local table = require(path .. "tablex") --shadow global table module
local set = class() local set = class({
name = "set",
})
--construct a new set --construct a new set
--elements is an optional ordered table of elements to be added to the set --elements is an optional ordered table of elements to be added to the set
function set:new(elements) function set:new(elements)
self = self:init({ self._keyed = {}
_keyed = {}, self._ordered = {}
_ordered = {},
})
if elements then if elements then
for _, v in ipairs(elements) do for _, v in ipairs(elements) do
self:add(v) self:add(v)
end end
end end
return self
end end
--check if an element is present in the set --check if an element is present in the set
@ -120,7 +119,7 @@ end
--copy a set --copy a set
function set:copy() function set:copy()
return set:new():add_set(self) return set():add_set(self)
end end
--create a new set containing the complement of the other set contained in this one --create a new set containing the complement of the other set contained in this one
@ -141,7 +140,7 @@ end
--create a new set containing the intersection of this set with another --create a new set containing the intersection of this set with another
--only the elements present in both sets will remain in the result --only the elements present in both sets will remain in the result
function set:intersection(other) function set:intersection(other)
local r = set:new() local r = set()
for i, v in self:ipairs() do for i, v in self:ipairs() do
if other:has(v) then if other:has(v) then
r:add(v) r:add(v)
@ -157,7 +156,7 @@ end
--equal to self:union(other):subtract_set(self:intersection(other)) --equal to self:union(other):subtract_set(self:intersection(other))
-- but with much less wasted effort -- but with much less wasted effort
function set:symmetric_difference(other) function set:symmetric_difference(other)
local r = set:new() local r = set()
for i, v in self:ipairs() do for i, v in self:ipairs() do
if not other:has(v) then if not other:has(v) then
r:add(v) r:add(v)

View File

@ -26,17 +26,15 @@
local path = (...):gsub("state_machine", "") local path = (...):gsub("state_machine", "")
local class = require(path .. "class") local class = require(path .. "class")
local state_machine = class() local state_machine = class({
name = "state_machine",
})
function state_machine:new(states, start_in_state) function state_machine:new(states, start_in_state)
self = self:init({ self.states = states or {}
states = states or {}, self.current_state_name = ""
current_state_name = "", self.reset_state_name = start_in_state or ""
reset_state_name = start_in_state or "",
})
self:reset() self:reset()
return self
end end
--get the current state table (or nil if it doesn't exist) --get the current state table (or nil if it doesn't exist)

View File

@ -9,19 +9,20 @@
local path = (...):gsub("timer", "") local path = (...):gsub("timer", "")
local class = require(path .. "class") local class = require(path .. "class")
local timer = class() local timer = class({
name = "timer",
})
--create a timer, with optional callbacks --create a timer, with optional callbacks
--callbacks recieve as arguments: --callbacks recieve as arguments:
-- the current progress as a number from 0 to 1, so can be used for lerps -- the current progress as a number from 0 to 1, so can be used for lerps
-- the timer object, so can be reset if needed -- the timer object, so can be reset if needed
function timer:new(time, on_progress, on_finish) function timer:new(time, on_progress, on_finish)
return self:init({ self.time = 0
time = 0, --set in the reset below self.timer = 0
timer = 0, self.on_progress = on_progress
on_progress = on_progress, self.on_finish = on_finish
on_finish = on_finish, self:reset(time)
}):reset(time)
end end
--update this timer, calling the relevant callback if it exists --update this timer, calling the relevant callback if it exists

100
vec2.lua
View File

@ -5,115 +5,67 @@
local path = (...):gsub("vec2", "") local path = (...):gsub("vec2", "")
local class = require(path .. "class") local class = require(path .. "class")
local math = require(path .. "mathx") --shadow global math module local math = require(path .. "mathx") --shadow global math module
local make_pooled = require(path .. "make_pooled")
local vec2 = class() local vec2 = class({
vec2.type = "vec2" name = "vec2",
})
--stringification --stringification
vec2.__mt.__tostring = function(self) function vec2:__tostring()
return ("(%.2f, %.2f)"):format(self.x, self.y) return ("(%.2f, %.2f)"):format(self.x, self.y)
end end
--probably-too-flexible ctor --probably-too-flexible ctor
function vec2:new(x, y) function vec2:new(x, y)
if type(x) == "number" and type(y) == "number" then if type(x) == "number" or type(x) == "nil" then
return vec2:xy(x,y) self:sset(x or 0, y)
elseif x then elseif type(x) == "table" then
if type(x) == "number" then if x.type and x:type() == "vec2" then
return vec2:filled(x) self:vset(x)
elseif type(x) == "table" then elseif x[1] then
if x.type == "vec2" then self:sset(x[1], x[2])
return x:copy() else
elseif x[1] and x[2] then self:sset(x.x, x.y)
return vec2:xy(x[1], x[2])
elseif x.x and x.y then
return vec2:xy(x.x, x.y)
end
end end
end end
return vec2:zero()
end end
--explicit ctors --explicit ctors
function vec2:copy() function vec2:copy()
return self:init({ return vec2(self.x, self.y)
x = self.x, y = self.y
})
end end
function vec2:xy(x, y) function vec2:xy(x, y)
return self:init({ return vec2(x, y)
x = x, y = y
})
end end
function vec2:filled(v) function vec2:filled(v)
return self:init({ return vec2(v)
x = v, y = v
})
end end
function vec2:zero() function vec2:zero()
return vec2:filled(0) return vec2(0)
end
--shared pooled storage
local _vec2_pool = {}
--size limit for tuning memory upper bound
local _vec2_pool_limit = 128
function vec2.pool_size()
return #_vec2_pool
end
--flush the entire pool
function vec2.flush_pool()
if vec2.pool_size() > 0 then
_vec2_pool = {}
end
end
--drain one element from the pool, if it exists
function vec2.drain_pool()
if #_vec2_pool > 0 then
return table.remove(_vec2_pool)
end
return nil
end
--get a pooled vector (initialise it yourself)
function vec2:pooled()
return vec2.drain_pool() or vec2:zero()
end
--get a pooled copy of an existing vector
function vec2:pooled_copy()
return vec2:pooled():vset(self)
end
--release a vector to the pool
function vec2:release(...)
if vec2.pool_size() < _vec2_pool_limit then
table.insert(_vec2_pool, self)
end
if ... then
vec2.release(...)
end
end end
--unpack for multi-args --unpack for multi-args
function vec2:unpack() function vec2:unpack()
return self.x, self.y return self.x, self.y
end end
--pack when a sequence is needed --pack when a sequence is needed
--(not particularly useful)
function vec2:pack() function vec2:pack()
return {self:unpack()} return {self:unpack()}
end end
--shared pooled storage
make_pooled(vec2, 128)
--get a pooled copy of an existing vector
function vec2:pooled_copy()
return vec2:pooled():vset(self)
end
--modify --modify
function vec2:sset(x, y) function vec2:sset(x, y)

103
vec3.lua
View File

@ -7,118 +7,73 @@ local path = (...):gsub("vec3", "")
local class = require(path .. "class") local class = require(path .. "class")
local vec2 = require(path .. "vec2") local vec2 = require(path .. "vec2")
local math = require(path .. "mathx") --shadow global math module local math = require(path .. "mathx") --shadow global math module
local make_pooled = require(path .. "make_pooled")
local vec3 = class() local vec3 = class({
vec3.type = "vec3" name = "vec3",
})
--stringification --stringification
vec3.__mt.__tostring = function(self) function vec3:__tostring()
return ("(%.2f, %.2f, %.2f)"):format(self.x, self.y, self.z) return ("(%.2f, %.2f, %.2f)"):format(self.x, self.y, self.z)
end end
--probably-too-flexible ctor --probably-too-flexible ctor
function vec3:new(x, y, z) function vec3:new(x, y, z)
if x and y and z then if type(x) == "number" or type(x) == "nil" then
return vec3:xyz(x, y, z) self:sset(x or 0, y, z)
elseif x then elseif type(x) == "table" then
if type(x) == "number" then if x.type and x:type() == "vec3" then
return vec3:filled(x) self:vset(x)
elseif type(x) == "table" then elseif x[1] then
if x.type == "vec3" then self:sset(x[1], x[2], x[3])
return x:copy() else
elseif x[1] and x[2] and x[3] then self:sset(x.x, x.y, x.z)
return vec3:xyz(x[1], x[2], x[3])
end
end end
end end
return vec3:zero()
end end
--explicit ctors --explicit ctors
function vec3:copy() function vec3:copy()
return self:init({ return vec3(self.x, self.y, self.z)
x = self.x, y = self.y, z = self.z
})
end end
function vec3:xyz(x, y, z) function vec3:xyz(x, y, z)
return self:init({ return vec3(x, y, z)
x = x, y = y, z = z
})
end end
function vec3:filled(v) function vec3:filled(x, y, z)
return self:init({ return vec3(x, y, z)
x = v, y = v, z = v
})
end end
function vec3:zero() function vec3:zero()
return vec3:filled(0) return vec3(0, 0, 0)
end end
--shared pooled storage --unpack for multi-args
local _vec3_pool = {} function vec3:unpack()
--size limit for tuning memory upper bound return self.x, self.y, self.z
local _vec3_pool_limit = 128
function vec3.pool_size()
return #_vec3_pool
end end
--flush the entire pool --pack when a sequence is needed
function vec3.flush_pool() function vec3:pack()
if vec3.pool_size() > 0 then return {self:unpack()}
_vec3_pool = {}
end
end end
--drain one element from the pool, if it exists --handle pooling
function vec3.drain_pool() make_pooled(vec3, 128)
if #_vec3_pool > 0 then
return table.remove(_vec3_pool)
end
return nil
end
--get a pooled vector (initialise it yourself)
function vec3:pooled()
return vec3.drain_pool() or vec3:zero()
end
--get a pooled copy of an existing vector --get a pooled copy of an existing vector
function vec3:pooled_copy() function vec3:pooled_copy()
return vec3:pooled():vset(self) return vec3:pooled():vset(self)
end end
--release a vector to the pool
function vec3:release()
if vec3.pool_size() < _vec3_pool_limit then
table.insert(_vec3_pool, self)
end
end
--unpack for multi-args
function vec3:unpack()
return self.x, self.y, self.z
end
--pack when a sequence is needed
--(not particularly useful)
function vec3:pack()
return {self:unpack()}
end
--modify --modify
function vec3:sset(x, y, z) function vec3:sset(x, y, z)
if not y then y = x end
if not z then z = y end
self.x = x self.x = x
self.y = y self.y = y or x
self.z = z self.z = z or y or z
return self return self
end end