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 class = require(path .. "class")
local async = class()
local async = class({
name = "async",
})
function async:new()
return self:init({
tasks = {},
tasks_stalled = {},
})
self.tasks = {}
self.tasks_stalled = {}
end
--add a task to the kernel
@ -138,4 +138,4 @@ function async:add_interval(f, delay)
end)
end
return async
return async

176
class.lua
View File

@ -1,83 +1,131 @@
--[[
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)
local c = {}
--class metatable
setmetatable(c, {
--wire up call as ctor
__call = function(self, ...)
return self:new(...)
end,
--handle single inheritence chain
__index = inherits,
})
--instance metatable
c.__mt = {
__index = c,
}
--common class functions
--generate unique increasing class ids
local class_id_gen = 0
local function next_class_id()
class_id_gen = class_id_gen + 1
return class_id_gen
end
--internal initialisation
--sets up an initialised object with a default value table
--performing a super construction if necessary, and (re-)assigning the right metatable
function c:init(t, ...)
if inherits and inherits.new then
--construct superclass instance, then overlay args table
local ct = inherits:new(...)
for k,v in pairs(t) do
ct[k] = v
end
t = ct
--implement an interface into c
local function implement(c, interface)
c.__is[interface] = true
for k, v in pairs(interface) do
if c[k] == nil and type(v) == "function" then
c[k] = v
end
--upgrade to this class and return
return setmetatable(t, self.__mt)
end
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
--constructor
--generally to be overridden
function c:new()
return self:init({})
if config.default_tostring then
function c:__tostring()
return name
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
--get the inherited class for super calls if/as needed
--allows overrides that still refer to superclass behaviour
function c:super()
return inherits
c.__super = extends
--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
--delegate a call to the superclass, by name
--still a bit clumsy but much cleaner than the inline equivalent,
--plus handles heirarchical complications, and detects various mistakes
function c:super_call(func_name, ...)
--
if type(func_name) ~= "string" then
error("super_call requires a string function name to look up, got "..tostring(func_name))
--implement all the passed interfaces/mixins
--in order provided
if implements then
for _, interface in ipairs(implements) do
implement(c, interface)
end
--todo: memoize the below :)
local previous_impl = c:super()
--find the first superclass that actually has the method
while previous_impl and not rawget(previous_impl, func_name) do
previous_impl = previous_impl:super()
end
if not previous_impl then
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
--default constructor, just proxy to the super constructor
--override it and use to set up the properties of the instance
--but don't forget to call the super constructor!
function c:new(...)
self:super(...)
end
--done

View File

@ -39,6 +39,7 @@ local _batteries = {
manual_gc = require_relative("manual_gc"),
colour = require_relative("colour"),
pretty = require_relative("pretty"),
make_pooled = require_relative("make_pooled"),
}
--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 class = require(path .. "class")
local pubsub = class()
local pubsub = class({
name = "pubsub",
})
--create a new pubsub bus
function pubsub:new()
return self:init({
subscriptions = {},
})
self.subscriptions = {}
end
--(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.
- [`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`...
- [`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.

View File

@ -6,12 +6,19 @@
]]
local path = (...):gsub("sequence", "")
local class = require(path .. "class")
local table = require(path .. "tablex") --shadow global table module
local functional = require(path .. "functional")
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
--(no pairs, sequences are ordered)
@ -21,7 +28,7 @@ sequence.iterate = ipairs
--upgrade a table into a sequence, or create a new sequence
function sequence:new(t)
return self:init(t or {})
return setmetatable(t or {}, sequence)
end
--sorting default to stable

17
set.lua
View File

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

View File

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

View File

@ -9,19 +9,20 @@
local path = (...):gsub("timer", "")
local class = require(path .. "class")
local timer = class()
local timer = class({
name = "timer",
})
--create a timer, with optional callbacks
--callbacks recieve as arguments:
-- 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
function timer:new(time, on_progress, on_finish)
return self:init({
time = 0, --set in the reset below
timer = 0,
on_progress = on_progress,
on_finish = on_finish,
}):reset(time)
self.time = 0
self.timer = 0
self.on_progress = on_progress
self.on_finish = on_finish
self:reset(time)
end
--update this timer, calling the relevant callback if it exists

100
vec2.lua
View File

@ -5,115 +5,67 @@
local path = (...):gsub("vec2", "")
local class = require(path .. "class")
local math = require(path .. "mathx") --shadow global math module
local make_pooled = require(path .. "make_pooled")
local vec2 = class()
vec2.type = "vec2"
local vec2 = class({
name = "vec2",
})
--stringification
vec2.__mt.__tostring = function(self)
function vec2:__tostring()
return ("(%.2f, %.2f)"):format(self.x, self.y)
end
--probably-too-flexible ctor
function vec2:new(x, y)
if type(x) == "number" and type(y) == "number" then
return vec2:xy(x,y)
elseif x then
if type(x) == "number" then
return vec2:filled(x)
elseif type(x) == "table" then
if x.type == "vec2" then
return x:copy()
elseif x[1] and x[2] then
return vec2:xy(x[1], x[2])
elseif x.x and x.y then
return vec2:xy(x.x, x.y)
end
if type(x) == "number" or type(x) == "nil" then
self:sset(x or 0, y)
elseif type(x) == "table" then
if x.type and x:type() == "vec2" then
self:vset(x)
elseif x[1] then
self:sset(x[1], x[2])
else
self:sset(x.x, x.y)
end
end
return vec2:zero()
end
--explicit ctors
function vec2:copy()
return self:init({
x = self.x, y = self.y
})
return vec2(self.x, self.y)
end
function vec2:xy(x, y)
return self:init({
x = x, y = y
})
return vec2(x, y)
end
function vec2:filled(v)
return self:init({
x = v, y = v
})
return vec2(v)
end
function vec2:zero()
return vec2:filled(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
return vec2(0)
end
--unpack for multi-args
function vec2:unpack()
return self.x, self.y
end
--pack when a sequence is needed
--(not particularly useful)
function vec2:pack()
return {self:unpack()}
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
function vec2:sset(x, y)

103
vec3.lua
View File

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