From 3cc177a0c087a7b03b966acbcba482800e1619cb Mon Sep 17 00:00:00 2001 From: Max Cahill <1bardesign@gmail.com> Date: Thu, 15 Jul 2021 16:09:08 +1000 Subject: [PATCH] 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 --- async.lua | 12 ++-- class.lua | 176 +++++++++++++++++++++++++++++----------------- init.lua | 1 + make_pooled.lua | 43 +++++++++++ pubsub.lua | 8 +-- readme.md | 2 +- sequence.lua | 13 +++- set.lua | 17 +++-- state_machine.lua | 16 ++--- timer.lua | 15 ++-- vec2.lua | 100 +++++++------------------- vec3.lua | 103 ++++++++------------------- 12 files changed, 255 insertions(+), 251 deletions(-) create mode 100644 make_pooled.lua diff --git a/async.lua b/async.lua index 78f325b..c350dae 100644 --- a/async.lua +++ b/async.lua @@ -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 \ No newline at end of file +return async diff --git a/class.lua b/class.lua index ef6a8b6..b0fec0c 100644 --- a/class.lua +++ b/class.lua @@ -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 diff --git a/init.lua b/init.lua index 3268996..fef7329 100644 --- a/init.lua +++ b/init.lua @@ -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 diff --git a/make_pooled.lua b/make_pooled.lua new file mode 100644 index 0000000..ef815d6 --- /dev/null +++ b/make_pooled.lua @@ -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 diff --git a/pubsub.lua b/pubsub.lua index a877f07..e0220ec 100644 --- a/pubsub.lua +++ b/pubsub.lua @@ -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) diff --git a/readme.md b/readme.md index a0e8c78..1f5adcd 100644 --- a/readme.md +++ b/readme.md @@ -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. diff --git a/sequence.lua b/sequence.lua index 36af9b9..49fd7cc 100644 --- a/sequence.lua +++ b/sequence.lua @@ -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 diff --git a/set.lua b/set.lua index 0867c85..4b552a9 100644 --- a/set.lua +++ b/set.lua @@ -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) diff --git a/state_machine.lua b/state_machine.lua index 587700e..c09aa1b 100644 --- a/state_machine.lua +++ b/state_machine.lua @@ -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) diff --git a/timer.lua b/timer.lua index 9697da0..961ac6c 100644 --- a/timer.lua +++ b/timer.lua @@ -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 diff --git a/vec2.lua b/vec2.lua index 4d64ba1..9cd1aa6 100644 --- a/vec2.lua +++ b/vec2.lua @@ -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) diff --git a/vec3.lua b/vec3.lua index a8f47fb..b1a7b72 100644 --- a/vec3.lua +++ b/vec3.lua @@ -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