[modified] big refactor to localised modules and require("batteries"):export() for global usage; renamed files table to tablex and math to mathx to allow using the library in your module root if you really want to.

This commit is contained in:
Max Cahill 2020-04-07 13:49:10 +10:00
parent 4bbbdf86d6
commit 6386aaf298
11 changed files with 263 additions and 281 deletions

View File

@ -14,14 +14,16 @@
cancelling cancelling
]] ]]
local async = {} local path = (...):gsub("async", "")
async._mt = {__index = async} local class = require(path .. "class")
local async = class()
function async:new() function async:new()
return setmetatable({ return self:init({
tasks = {}, tasks = {},
tasks_stalled = {}, tasks_stalled = {},
}, self._mt) })
end end
--add a task to the kernel --add a task to the kernel

View File

@ -1,7 +1,7 @@
--[[ --[[
colour handling stuff colour handling stuff
feel free to alias to colour feel free to alias to `color` :)
]] ]]
local bit = require("bit") local bit = require("bit")

View File

@ -9,17 +9,18 @@
reduce has a similar problem, but at least arguments reduce has a similar problem, but at least arguments
there are clear! there are clear!
optional:
set BATTERIES_FUNCTIONAL_MODULE to a table before requiring
if you don't want this to modify the global `table` table
]] ]]
local _table = BATTERIES_FUNCTIONAL_MODULE or table local path = (...):gsub("functional", "")
local tablex = require(path .. "tablex")
local functional = setmetatable({}, {
__index = tablex,
})
--simple sequential iteration, f is called for all elements of t --simple sequential iteration, f is called for all elements of t
--f can return non-nil to break the loop (and return the value) --f can return non-nil to break the loop (and return the value)
function _table.foreach(t, f) function functional.foreach(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
local r = f(v, i) local r = f(v, i)
if r ~= nil then if r ~= nil then
@ -31,7 +32,7 @@ end
--performs a left to right reduction of t using f, with o as the initial value --performs a left to right reduction of t using f, with o as the initial value
-- reduce({1, 2, 3}, f, 0) -> f(f(f(0, 1), 2), 3) -- reduce({1, 2, 3}, f, 0) -> f(f(f(0, 1), 2), 3)
-- (but performed iteratively, so no stack smashing) -- (but performed iteratively, so no stack smashing)
function _table.reduce(t, f, o) function functional.reduce(t, f, o)
for i,v in ipairs(t) do for i,v in ipairs(t) do
o = f(o, v) o = f(o, v)
end end
@ -40,7 +41,7 @@ end
--maps a sequence {a, b, c} -> {f(a), f(b), f(c)} --maps a sequence {a, b, c} -> {f(a), f(b), f(c)}
-- (automatically drops any nils due to table.insert, which can be used to simultaneously map and filter) -- (automatically drops any nils due to table.insert, which can be used to simultaneously map and filter)
function _table.map(t, f) function functional.map(t, f)
local r = {} local r = {}
for i,v in ipairs(t) do for i,v in ipairs(t) do
local mapped = f(v, i) local mapped = f(v, i)
@ -54,7 +55,7 @@ end
--maps a sequence inplace, modifying it {a, b, c} -> {f(a), f(b), f(c)} --maps a sequence inplace, modifying it {a, b, c} -> {f(a), f(b), f(c)}
-- (automatically drops any nils, which can be used to simultaneously map and filter, -- (automatically drops any nils, which can be used to simultaneously map and filter,
-- but this results in a linear table.remove so "careful" for big working sets) -- but this results in a linear table.remove so "careful" for big working sets)
function _table.remap(t, f) function functional.remap(t, f)
local i = 1 local i = 1
while i <= #t do while i <= #t do
local mapped = f(t[i]) local mapped = f(t[i])
@ -69,7 +70,7 @@ function _table.remap(t, f)
end end
--filters a sequence --filters a sequence
function _table.filter(t, f) function functional.filter(t, f)
local r = {} local r = {}
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v, i) then if f(v, i) then
@ -80,7 +81,7 @@ function _table.filter(t, f)
end end
--partitions a sequence based on filter criteria --partitions a sequence based on filter criteria
function _table.partition(t, f) function functional.partition(t, f)
local a = {} local a = {}
local b = {} local b = {}
for i,v in ipairs(t) do for i,v in ipairs(t) do
@ -97,7 +98,7 @@ end
--iteration limited by min(#t1, #t2) --iteration limited by min(#t1, #t2)
--function receives arguments (t1, t2, i) --function receives arguments (t1, t2, i)
--nil results ignored --nil results ignored
function _table.zip(t1, t2, f) function functional.zip(t1, t2, f)
local ret = {} local ret = {}
local limit = math.min(#t2, #t2) local limit = math.min(#t2, #t2)
for i=1, limit do for i=1, limit do
@ -114,9 +115,9 @@ end
--return a copy of a sequence with all duplicates removed --return a copy of a sequence with all duplicates removed
-- causes a little "extra" gc churn; one table and one closure -- causes a little "extra" gc churn; one table and one closure
-- as well as the copied deduped table -- as well as the copied deduped table
function _table.dedupe(t) function functional.dedupe(t)
local seen = {} local seen = {}
return _table.filter(t, function(v) return functional.filter(t, function(v)
if seen[v] then if seen[v] then
return false return false
end end
@ -126,7 +127,7 @@ function _table.dedupe(t)
end end
--append sequence t2 into t1, modifying t1 --append sequence t2 into t1, modifying t1
function _table.append_inplace(t1, t2) function functional.append_inplace(t1, t2)
for i,v in ipairs(t2) do for i,v in ipairs(t2) do
table.insert(t1, v) table.insert(t1, v)
end end
@ -134,10 +135,10 @@ function _table.append_inplace(t1, t2)
end end
--return a new sequence with the elements of both t1 and t2 --return a new sequence with the elements of both t1 and t2
function _table.append(t1, t2) function functional.append(t1, t2)
local r = {} local r = {}
append_inplace(r, t1) functional.append_inplace(r, t1)
append_inplace(r, t2) functional.append_inplace(r, t2)
return r return r
end end
@ -146,7 +147,7 @@ end
----------------------------------------------------------- -----------------------------------------------------------
--true if any element of the table matches f --true if any element of the table matches f
function _table.any(t, f) function functional.any(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v) then if f(v) then
return true return true
@ -156,7 +157,7 @@ function _table.any(t, f)
end end
--true if no element of the table matches f --true if no element of the table matches f
function _table.none(t, f) function functional.none(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v) then if f(v) then
return false return false
@ -166,7 +167,7 @@ function _table.none(t, f)
end end
--true if all elements of the table match f --true if all elements of the table match f
function _table.all(t, f) function functional.all(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
if not f(v) then if not f(v) then
return false return false
@ -176,7 +177,7 @@ function _table.all(t, f)
end end
--counts the elements of t that match f --counts the elements of t that match f
function _table.count(t, f) function functional.count(t, f)
local c = 0 local c = 0
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v) then if f(v) then
@ -187,7 +188,7 @@ function _table.count(t, f)
end end
--true if the table contains element e --true if the table contains element e
function _table.contains(t, e) function functional.contains(t, e)
for i, v in ipairs(t) do for i, v in ipairs(t) do
if v == e then if v == e then
return true return true
@ -197,26 +198,26 @@ function _table.contains(t, e)
end end
--return the numeric sum of all elements of t --return the numeric sum of all elements of t
function _table.sum(t) function functional.sum(t)
return _table.reduce(t, function(a, b) return functional.reduce(t, function(a, b)
return a + b return a + b
end, 0) end, 0)
end end
--return the numeric mean of all elements of t --return the numeric mean of all elements of t
function _table.mean(t) function functional.mean(t)
local len = #t local len = #t
if len == 0 then if len == 0 then
return 0 return 0
end end
return _table.sum(t) / len return functional.sum(t) / len
end end
--return the minimum and maximum of t in one pass --return the minimum and maximum of t in one pass
--or zero for both if t is empty --or zero for both if t is empty
-- (would perhaps more correctly be math.huge, -math.huge -- (would perhaps more correctly be math.huge, -math.huge
-- but that tends to be surprising/annoying in practice) -- but that tends to be surprising/annoying in practice)
function _table.minmax(t) function functional.minmax(t)
local max, min local max, min
for i,v in ipairs(t) do for i,v in ipairs(t) do
min = not min and v or math.min(min, v) min = not min and v or math.min(min, v)
@ -230,26 +231,26 @@ function _table.minmax(t)
end end
--return the maximum element of t or zero if t is empty --return the maximum element of t or zero if t is empty
function _table.max(t) function functional.max(t)
local min, max = _table.minmax(t) local min, max = functional.minmax(t)
return max return max
end end
--return the minimum element of t or zero if t is empty --return the minimum element of t or zero if t is empty
function _table.min(t) function functional.min(t)
local min, max = _table.minmax(t) local min, max = functional.minmax(t)
return min return min
end end
--return the element of the table that results in the lowest numeric value --return the element of the table that results in the lowest numeric value
--(function receives element and index respectively) --(function receives element and index respectively)
function _table.find_min(t, f) function functional.find_min(t, f)
local current = nil local current = nil
local current_best = math.huge local current_min = math.huge
for i, v in ipairs(t) do for i, e in ipairs(t) do
v = f(v, i) local v = f(e, i)
if v and v < current_best then if v and v < current_min then
current_best = v current_min = v
current = e current = e
end end
end end
@ -258,13 +259,13 @@ end
--return the element of the table that results in the greatest numeric value --return the element of the table that results in the greatest numeric value
--(function receives element and index respectively) --(function receives element and index respectively)
function _table.find_max(t, f) function functional.find_max(t, f)
local current = nil local current = nil
local current_best = -math.huge local current_max = -math.huge
for i, v in ipairs(t) do for i, e in ipairs(t) do
v = f(v, i) local v = f(e, i)
if v and v > current_best then if v and v > current_max then
current_best = v current_max = v
current = e current = e
end end
end end
@ -272,18 +273,18 @@ function _table.find_max(t, f)
end end
--alias --alias
_table.find_best = _table.find_max functional.find_best = functional.find_max
--return the element of the table that results in the value nearest to the passed value --return the element of the table that results in the value nearest to the passed value
--todo: optimise as this generates a closure each time --todo: optimise as this generates a closure each time
function _table.find_nearest(t, f, v) function functional.find_nearest(t, f, v)
return _table.find_best(t, function(e) return functional.find_best(t, function(e)
return -math.abs(f(e) - v) return -math.abs(f(e) - v)
end) end)
end end
--return the first element of the table that results in a true filter --return the first element of the table that results in a true filter
function _table.find_match(t, f) function functional.find_match(t, f)
for i,v in ipairs(t) do for i,v in ipairs(t) do
if f(v) then if f(v) then
return v return v
@ -292,4 +293,4 @@ function _table.find_match(t, f)
return nil return nil
end end
return _table return functional

View File

@ -24,18 +24,11 @@ local function require_relative(p)
return require(table.concat({path, p}, ".")) return require(table.concat({path, p}, "."))
end end
if BATTERIES_NO_GLOBALS then
--define local tables for everything to go into
BATTERIES_MATH_MODULE = {}
BATTERIES_TABLE_MODULE = {}
BATTERIES_FUNCTIONAL_MODULE = {}
end
local _class = require_relative("class") local _class = require_relative("class")
local _math = require_relative("math") local _mathx = require_relative("mathx")
local _table = require_relative("table") local _tablex = require_relative("tablex")
local _stable_sort = require_relative("stable_sort") local _stable_sort = require_relative("stable_sort")
local _functional = require_relative("functional") local _functional = require_relative("functional")
@ -54,15 +47,63 @@ local _manual_gc = require_relative("manual_gc")
local _colour = require_relative("colour") local _colour = require_relative("colour")
--export globally if required --build the module
if not BATTERIES_NO_GLOBALS then local _batteries = {
--fire and forget mode function
export = export,
--
class = _class,
--support x and non-x naming
math = _mathx,
mathx = _mathx,
--
table = _tablex,
tablex = _tablex,
--sorting routines
stable_sort = _stable_sort,
sort = _stable_sort,
--
functional = _functional,
--
sequence = _sequence,
--
vec2 = _vec2,
vec3 = _vec3,
intersect = _intersect,
--
unique_mapping = _unique_mapping,
state_machine = _state_machine,
async = _async,
manual_gc = _manual_gc,
colour = _colour,
color = _colour,
}
--easy export globally if required
function _batteries:export(self)
--export oo
class = _class class = _class
--overlay tablex and functional and sort routines onto table
_tablex.overlay(table, _tablex)
_tablex.overlay(table, _functional)
_stable_sort:export()
--functional module also available separate from table
functional = _functional
--export sequence
sequence = _sequence sequence = _sequence
--overlay onto math
_tablex.overlay(math, _mathx)
--export geom
vec2 = _vec2 vec2 = _vec2
vec3 = _vec3 vec3 = _vec3
intersect = _intersect intersect = _intersect
--misc :)
unique_mapping = _unique_mapping unique_mapping = _unique_mapping
state_machine = _state_machine state_machine = _state_machine
async = _async async = _async
@ -71,23 +112,11 @@ if not BATTERIES_NO_GLOBALS then
--support both spellings --support both spellings
colour = _colour colour = _colour
color = _colour color = _colour
--export top level module as well for ease of migration for code
batteries = _batteries
return self
end end
--either way, export to package registry return _batteries
return {
class = _class,
math = _math,
table = _table,
stable_sort = _stable_sort,
functional = _functional,
sequence = _sequence,
vec2 = _vec2,
vec3 = _vec3,
intersect = _intersect,
unique_mapping = _unique_mapping,
state_machine = _state_machine,
async = _async,
manual_gc = _manual_gc,
colour = _colour,
color = _colour,
}

View File

@ -1,15 +1,13 @@
--[[ --[[
extra mathematical functions extra mathematical functions
optional:
set BATTERIES_MATH_MODULE to a table before requiring
if you don't want this to modify the global `math` table
]] ]]
local _math = BATTERIES_MATH_MODULE or math local mathx = setmetatable({}, {
__index = math,
})
--wrap v around range [lo, hi) --wrap v around range [lo, hi)
function _math.wrap(v, lo, hi) function mathx.wrap(v, lo, hi)
local range = hi - lo local range = hi - lo
local relative = v - lo local relative = v - lo
local relative_wrapped = relative % range local relative_wrapped = relative % range
@ -19,46 +17,47 @@ function _math.wrap(v, lo, hi)
end end
--clamp v to range [lo, hi] --clamp v to range [lo, hi]
function _math.clamp(v, lo, hi) function mathx.clamp(v, lo, hi)
return math.max(lo, math.min(v, hi)) return math.max(lo, math.min(v, hi))
end end
function _math.clamp01(v) --clamp v to range [0, 1]
return _math.clamp(v, 0, 1) function mathx.clamp01(v)
return mathx.clamp(v, 0, 1)
end end
--round v to nearest whole --round v to nearest whole
function _math.round(v) function mathx.round(v)
return math.floor(v + 0.5) return math.floor(v + 0.5)
end end
--round v to one-in x --round v to one-in x
-- (eg x = 2, v rounded to increments of 0.5) -- (eg x = 2, v rounded to increments of 0.5)
function _math.to_one_in(v, x) function mathx.to_one_in(v, x)
return _math.round(v * x) / x return mathx.round(v * x) / x
end end
--round v to a given decimal precision --round v to a given decimal precision
function _math.to_precision(v, decimal_points) function mathx.to_precision(v, decimal_points)
return _math.to_one_in(v, math.pow(10, decimal_points)) return mathx.to_one_in(v, math.pow(10, decimal_points))
end end
--0, 1, -1 sign of a scalar --0, 1, -1 sign of a scalar
function _math.sign(v) function mathx.sign(v)
if v < 0 then return -1 end if v < 0 then return -1 end
if v > 0 then return 1 end if v > 0 then return 1 end
return 0 return 0
end end
--linear interpolation between a and b --linear interpolation between a and b
function _math.lerp(a, b, t) function mathx.lerp(a, b, t)
return a * (1.0 - t) + b * t return a * (1.0 - t) + b * t
end end
--linear interpolation with a minimum "final step" distance --linear interpolation with a minimum "final step" distance
--useful for making sure dynamic lerps do actually reach their final destination --useful for making sure dynamic lerps do actually reach their final destination
function _math.lerp_eps(a, b, t, eps) function mathx.lerp_eps(a, b, t, eps)
local v = math.lerp(a, b, t) local v = mathx.lerp(a, b, t)
if math.abs(v - b) < eps then if math.abs(v - b) < eps then
v = b v = b
end end
@ -67,13 +66,13 @@ end
--classic smoothstep --classic smoothstep
--(only "safe" for 0-1 range) --(only "safe" for 0-1 range)
function _math.smoothstep(v) function mathx.smoothstep(v)
return v * v * (3 - 2 * v) return v * v * (3 - 2 * v)
end end
--classic smootherstep; zero 2nd order derivatives at 0 and 1 --classic smootherstep; zero 2nd order derivatives at 0 and 1
--(only safe for 0-1 range) --(only safe for 0-1 range)
function _math.smootherstep(v) function mathx.smootherstep(v)
return v * v * v * (v * (v * 6 - 15) + 10) return v * v * v * (v * (v * 6 - 15) + 10)
end end
@ -139,7 +138,7 @@ local sparse_primes_1k = {
6689, 7039, 7307, 7559, 7573, 7919, 6689, 7039, 7307, 7559, 7573, 7919,
} }
function _math.first_above(v, t) function mathx.first_above(v, t)
for _,p in ipairs(t) do for _,p in ipairs(t) do
if p > v then if p > v then
return p return p
@ -148,30 +147,30 @@ function _math.first_above(v, t)
return t[#t] return t[#t]
end end
function _math.next_prime_1k(v) function mathx.next_prime_1k(v)
return _math.first_above(v, primes_1k) return mathx.first_above(v, primes_1k)
end end
function _math.next_prime_1k_sparse(v) function mathx.next_prime_1k_sparse(v)
return _math.first_above(v, sparse_primes_1k) return mathx.first_above(v, sparse_primes_1k)
end end
--angle handling stuff --angle handling stuff
function _math.normalise_angle(a) function mathx.normalise_angle(a)
return _math.wrap(a, -math.pi, math.pi) return mathx.wrap(a, -math.pi, math.pi)
end end
function _math.relative_angle(a1, a2) function mathx.relative_angle(a1, a2)
a1 = _math.normalise_angle(a1) a1 = mathx.normalise_angle(a1)
a2 = _math.normalise_angle(a2) a2 = mathx.normalise_angle(a2)
return _math.normalise_angle(a1 - a2) return mathx.normalise_angle(a1 - a2)
end end
--geometric rotation multi-return --geometric rotation multi-return
function _math.rotate(x, y, r) function mathx.rotate(x, y, r)
local s = math.sin(r) local s = math.sin(r)
local c = math.cos(r) local c = math.cos(r)
return c * x - s * y, s * x + c * y return c * x - s * y, s * x + c * y
end end
return _math return mathx

View File

@ -4,30 +4,30 @@ Core dependencies for making games with lua, especially with [love](https://love
Does a lot to get projects off the ground faster, filling out lua's sparse standard library a little and providing implementations of common algorithms and data structures useful for games. Does a lot to get projects off the ground faster, filling out lua's sparse standard library a little and providing implementations of common algorithms and data structures useful for games.
It's a bit of a grab bag of functionality, but quite extensively documented, and currently still under a hundred kb uncompressed, including the license and readme, so you get quite a lot per byte! Of course, feel free to trim it down for your use case as required. Many of the modules are "mostly" standalone. It's a bit of a grab bag of functionality, but quite extensively documented, and currently still under a hundred kb uncompressed, including the license and readme, so you get quite a lot per byte! Of course, feel free to trim it down for your use case as required (see [below](#stripping-down-batteries).
# Module Overview # Module Overview
- `class` - Single-inheritance oo in a single function. - `class` - Single-inheritance oo in a single function.
- `math` - Mathematical extensions. - `mathx` - Mathematical extensions. Alias `math`.
- `table` - Table handling extensions. - `tablex` - Table handling extensions. Alias `table`.
- `stable_sort` - A stable sorting algorithm that is also, as a bonus, faster than table.sort under luajit. - `stable_sort` - A stable sorting algorithm that is also, as a bonus, often faster than table.sort under luajit.
- `functional` - Functional programming facilities. `map`, `reduce`, `any`, `match`, `minmax`, `mean`... - `functional` - Functional programming facilities. `map`, `reduce`, `any`, `match`, `minmax`, `mean`...
- `sequence` - An oo wrapper on sequential tables so you can do `t:insert(i, v)` instead of `table.insert(t, i, v)`. Also supports chaining the functional interface above. - `sequence` - 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 typing!
- `vec2` - 2d vectors with method chaining, garbage saving interface. A bit of a mouthful at times. - `vec2` - 2d vectors with method chaining, garbage saving interface. A bit of a mouthful at times, but you get used to it.
- `vec3` - 3d vectors as above. - `vec3` - 3d vectors as above.
- `intersect` - 2d intersection routines, a bit sparse at the moment - `intersect` - 2d intersection routines, a bit sparse at the moment
- `unique_mapping` - Generate a unique mapping from arbitrary lua values to numeric keys - essentially making up a consistent ordering for unordered data. Niche, but useful for optimising draw batches for example, as you can't sort on textures without it. - `unique_mapping` - Generate a unique mapping from arbitrary lua values to numeric keys - essentially making up a consistent ordering for unordered data. Niche, but useful for optimising draw batches for example, as you can't sort on textures without it.
- `state_machine` - Finite state machine implementation with state transitions and all the rest. Useful for game states, ai, cutscenes... - `state_machine` - Finite state machine implementation with state transitions and all the rest. Useful for game states, ai, cutscenes...
- `async` - Async operations as coroutines. - `async` - Async operations as coroutines.
- `manual_gc` - Get GC out of your update/draw calls. Really good when trying to get accurate profiling information; no more spikes. Requires you to think a bit about your garbage budgets though. - `manual_gc` - Get GC out of your update/draw calls. Really good when trying to get accurate profiling information; no more spikes. Requires you to think a bit about your garbage budgets though.
- `colour` - Colour conversion routines. Can also be spelled `color` if you're into that. - `colour` - Colour conversion routines. Alias `color`.
# Todo/WIP list # Todo/WIP list
Endless, of course :) Endless, of course :)
- `string` - As for table and math, would be good to have a more filled out string handling API. - `string` - As for tablex and mathx, would be good to have a more filled out string handling API.
- `colour` - Bidirectional hsv conversion and friends would fit nicely here. - `colour` - Bidirectional hsv conversion and friends would fit nicely here.
- Geometry: - Geometry:
- `vec3` - Needs more fleshing out for serious use. - `vec3` - Needs more fleshing out for serious use.
@ -50,23 +50,25 @@ Pull requests are welcome for anything!
If you have something "big" to contribute please get in touch before starting work so we can make sure it fits, but I'm quite open minded! If you have something "big" to contribute please get in touch before starting work so we can make sure it fits, but I'm quite open minded!
# Globals? # Export Globals
By default `batteries` will modify builtin lua tables (such as `table` and `math`) as it sees fit, as this eases consumption later on - you don't have to remember if say, `table.remove_value` is built in to lua or not. As the intended use case is fairly pervasive, required early, and "fire and forget", this sits with me just fine. You are strongly encouraged to use the library in a "fire and forget" manner through `require("batteries"):export()` (or whatever appropriate module path), which will modify builtin lua modules (such as `table` and `math`) and expose all the modules directly as globals for convenience.
If however, you'd prefer to require things locally, you can (rather ironically) set a few globals at boot time as documented in each module to change this behaviour, or set `BATTERIES_NO_GLOBALS = true` to make none of them modify anything global. If you really want to you can undefine the behaviour-changing globals after the module is required, as the results are cached. This eases consumption later on - you don't have to remember if say, `table.remove_value` is built in to lua or not, or get used to accessing the builtin table functions through `batteries.table` or `tablex`.
I'd strongly recommend that if you find yourself defining the above, stop and think why/if you really want to avoid globals for a library intended to be commonly used across your entire codebase! While this will likely sit badly with anyone who's had "no globals!" hammered into them, I believe for `batteries` (and many foundational libraries) it makes sense to just import once at boot. You're going to be pulling it in almost everywhere anyway; why bother making yourself jump through more hoops.
Some folks will have good reasons, which is why the functionality is present! You can of course use the separate modules on their own, either with a single require for all of `batteries`, and use through something like `batteries.functional.map`, or requiring individual modules explicitly. This more careful approach _will_ let you be more clear about your dependencies, at the cost of more setup work needing to re-require batteries everywhere, or expose it as a global in the first place.
Others may wish to reconsider, and save themselves typing `batteries` a few hundred times :) I'd strongly recommend that if you find yourself frustrated with the above, stop and think why/if you really want to avoid globals for a library intended to be commonly used across your entire codebase! You may wish to reconsider, and save yourself typing `batteries` a few hundred times :)
# Why aren't various types using `class`? # Stripping down `batteries`
To avoid a dependency on class.lua for those modules when used in isolation. Many of the modules "just work" on their own if you just want to vendor in something specific.
Makes them a little bit more of a pain internally and requires more lines of code, but means you can vendor in just the code you need with very little effort. There are some inter-dependencies in the more complex modules, which should be straightforward to detect and figure out the best course of action (include or strip out) if you want to make a stripped-down version for distribution.
Currently the lib is 30kb or so compressed, including the readme.
# License # License

View File

@ -8,17 +8,18 @@
first as method calls. first as method calls.
]] ]]
local sequence = {} local path = (...):gsub("sequence", "")
local class = require(path .. "class")
sequence._mt = {__index = sequence} local table = require(path .. "tablex") --shadow global table module
local functional = require(path .. "functional")
local sequence = class()
--proxy missing table fns to global table api --proxy missing table fns to global table api
sequence.__mt = {__index = table} setmetatable(sequence, {__index = table})
setmetatable(sequence, sequence.__mt)
--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 setmetatable(t or {}, sequence._mt) return self:init(t or {})
end end
--alias --alias
@ -27,53 +28,50 @@ sequence.join = table.concat
--sorting default to stable if present --sorting default to stable if present
sequence.sort = table.stable_sort or table.sort sequence.sort = table.stable_sort or table.sort
--(handle functional module delegation correctly) --import functional interface to sequence in a type-preserving way, for method chaining
local _func = BATTERIES_FUNCTIONAL_MODULE or table
--import functional interface to sequence in a type preserving way
function sequence:keys() function sequence:keys()
return sequence:new(_func.keys(self)) return sequence:new(functional.keys(self))
end end
function sequence:values() function sequence:values()
return sequence:new(_func.values(self)) return sequence:new(functional.values(self))
end end
function sequence:foreach(f) function sequence:foreach(f)
return _func.foreach(self, f) return functional.foreach(self, f)
end end
function sequence:reduce(f, o) function sequence:reduce(f, o)
return _func.foreach(self, f, o) return functional.foreach(self, f, o)
end end
function sequence:map(f) function sequence:map(f)
return sequence:new(_func.map(self, f)) return sequence:new(functional.map(self, f))
end end
function sequence:remap(f) function sequence:remap(f)
return _func.remap(self, f) return functional.remap(self, f)
end end
function sequence:filter(f) function sequence:filter(f)
return sequence:new(_func.filter(self, f)) return sequence:new(functional.filter(self, f))
end end
function sequence:partition(f) function sequence:partition(f)
local a, b = _func.partition(self, f) local a, b = functional.partition(self, f)
return sequence:new(a), sequence:new(b) return sequence:new(a), sequence:new(b)
end end
function sequence:zip(other, f) function sequence:zip(other, f)
return sequence:new(_func.zip(self, other, f)) return sequence:new(functional.zip(self, other, f))
end end
function sequence:dedupe() function sequence:dedupe()
return _func.dedupe(self) return functional.dedupe(self)
end end
function sequence:append_inplace(other) function sequence:append_inplace(other)
return _func.append_inplace(self, other) return functional.append_inplace(self, other)
end end
function sequence:append(other) function sequence:append(other)
@ -81,7 +79,7 @@ function sequence:append(other)
end end
function sequence:copy(deep) function sequence:copy(deep)
return sequence:new(_func.copy(self, deep)) return sequence:new(functional.copy(self, deep))
end end
return sequence return sequence

View File

@ -1,20 +1,6 @@
-- stable sorting routines for lua --[[
-- stable sorting routines
-- by default modifies the global table namespace so you don't have ]]
-- to re-require it everywhere.
--
-- table.stable_sort
-- a fast stable sort
-- table.unstable_sort
-- alias for the builtin unstable table.sort
-- table.insertion_sort
-- an insertion sort, should you prefer it
--
-- alternatively, adds them to either BATTERIES_SORT_MODULE or
-- BATTERIES_TABLE_MODULE if defined
--
-- also returns just the interface above as a module either way
--
--this is based on MIT licensed code from Dirk Laurie and Steve Fisher --this is based on MIT licensed code from Dirk Laurie and Steve Fisher
--license as follows: --license as follows:
@ -43,13 +29,13 @@
-- (modifications by Max Cahill 2018, 2020) -- (modifications by Max Cahill 2018, 2020)
local _sort_core = {} local sort_core = {}
--tunable size for insertion sort "bottom out" --tunable size for insertion sort "bottom out"
_sort_core.max_chunk_size = 32 sort_core.max_chunk_size = 32
--insertion sort on a section of array --insertion sort on a section of array
function _sort_core._insertion_sort_impl(array, first, last, less) function sort_core._insertion_sort_impl(array, first, last, less)
for i = first + 1, last do for i = first + 1, last do
local k = first local k = first
local v = array[i] local v = array[i]
@ -66,7 +52,7 @@ function _sort_core._insertion_sort_impl(array, first, last, less)
end end
--merge sorted adjacent sections of array --merge sorted adjacent sections of array
function _sort_core._merge(array, workspace, low, middle, high, less) function sort_core._merge(array, workspace, low, middle, high, less)
local i, j, k local i, j, k
i = 1 i = 1
-- copy first half of array to auxiliary array -- copy first half of array to auxiliary array
@ -99,14 +85,14 @@ function _sort_core._merge(array, workspace, low, middle, high, less)
end end
--implementation for the merge sort --implementation for the merge sort
function _sort_core._merge_sort_impl(array, workspace, low, high, less) function sort_core._merge_sort_impl(array, workspace, low, high, less)
if high - low <= _sort_core.max_chunk_size then if high - low <= sort_core.max_chunk_size then
_sort_core._insertion_sort_impl(array, low, high, less) sort_core._insertion_sort_impl(array, low, high, less)
else else
local middle = math.floor((low + high) / 2) local middle = math.floor((low + high) / 2)
_sort_core._merge_sort_impl(array, workspace, low, middle, less) sort_core._merge_sort_impl(array, workspace, low, middle, less)
_sort_core._merge_sort_impl(array, workspace, middle + 1, high, less) sort_core._merge_sort_impl(array, workspace, middle + 1, high, less)
_sort_core._merge(array, workspace, low, middle, high, less) sort_core._merge(array, workspace, low, middle, high, less)
end end
end end
@ -116,7 +102,7 @@ local function default_less(a, b)
end end
--inline common setup stuff --inline common setup stuff
function _sort_core._sort_setup(array, less) function sort_core._sort_setup(array, less)
--default less --default less
less = less or default_less less = less or default_less
-- --
@ -133,37 +119,36 @@ function _sort_core._sort_setup(array, less)
return trivial, n, less return trivial, n, less
end end
function _sort_core.stable_sort(array, less) function sort_core.stable_sort(array, less)
--setup --setup
local trivial, n, less = _sort_core._sort_setup(array, less) local trivial, n, less = sort_core._sort_setup(array, less)
if not trivial then if not trivial then
--temp storage; allocate ahead of time --temp storage; allocate ahead of time
local workspace = {} local workspace = {}
local middle = math.ceil(n / 2) local middle = math.ceil(n / 2)
workspace[middle] = array[1] workspace[middle] = array[1]
--dive in --dive in
_sort_core._merge_sort_impl( array, workspace, 1, n, less ) sort_core._merge_sort_impl( array, workspace, 1, n, less )
end end
return array return array
end end
function _sort_core.insertion_sort(array, less) function sort_core.insertion_sort(array, less)
--setup --setup
local trivial, n, less = _sort_core._sort_setup(array, less) local trivial, n, less = sort_core._sort_setup(array, less)
if not trivial then if not trivial then
_sort_core._insertion_sort_impl(array, 1, n, less) sort_core._insertion_sort_impl(array, 1, n, less)
end end
return array return array
end end
_sort_core.unstable_sort = table.sort sort_core.unstable_sort = table.sort
--export sort core --export sort core to the global table module
function sort_core:export()
table.insertion_sort = sort_core.insertion_sort
table.stable_sort = sort_core.stable_sort
table.unstable_sort = sort_core.unstable_sort
end
local _table = BATTERIES_SORT_MODULE or BATTERIES_TABLE_MODULE or table return sort_core
_table.insertion_sort = _sort_core.insertion_sort
_table.stable_sort = _sort_core.stable_sort
_table.unstable_sort = _sort_core.unstable_sort
return _sort_core

View File

@ -1,54 +1,47 @@
--[[ --[[
extra table routines extra table routines
optional:
set BATTERIES_TABLE_MODULE to a table before requiring
if you don't want this to modify the global `table` table
]] ]]
local _table = BATTERIES_TABLE_MODULE or table
--apply prototype to module if it isn't the global table --apply prototype to module if it isn't the global table
--so it works "as if" it was the global table api --so it works "as if" it was the global table api
--upgraded with these routines --upgraded with these routines
if _table ~= table then
setmetatable(_table, { local tablex = setmetatable({}, {
__index = table, __index = table,
}) })
end
--alias --alias
_table.join = _table.concat tablex.join = tablex.concat
--return the back element of a table --return the back element of a table
function _table.back(t) function tablex.back(t)
return t[#t] return t[#t]
end end
--remove the back element of a table and return it --remove the back element of a table and return it
function _table.pop(t) function tablex.pop(t)
return table.remove(t) return table.remove(t)
end end
--insert to the back of a table --insert to the back of a table
function _table.push(t, v) function tablex.push(t, v)
return table.insert(t, v) return table.insert(t, v)
end end
--remove the front element of a table and return it --remove the front element of a table and return it
function _table.shift(t) function tablex.shift(t)
return table.remove(t, 1) return table.remove(t, 1)
end end
--insert to the front of a table --insert to the front of a table
function _table.unshift(t, v) function tablex.unshift(t, v)
return table.insert(t, 1, v) return table.insert(t, 1, v)
end end
--find the index in a sequential table that a resides at --find the index in a sequential table that a resides at
--or nil if nothing was found --or nil if nothing was found
--(todo: consider pairs version?) --(todo: consider pairs version?)
function _table.index_of(t, a) function tablex.index_of(t, a)
if a == nil then return nil end if a == nil then return nil end
for i,b in ipairs(t) do for i,b in ipairs(t) do
if a == b then if a == b then
@ -60,8 +53,8 @@ end
--remove the first instance of value from a table (linear search) --remove the first instance of value from a table (linear search)
--returns true if the value was removed, else false --returns true if the value was removed, else false
function _table.remove_value(t, a) function tablex.remove_value(t, a)
local i = _table.index_of(t, a) local i = tablex.index_of(t, a)
if i then if i then
table.remove(t, i) table.remove(t, i)
return true return true
@ -71,8 +64,8 @@ end
--add a value to a table if it doesn't already exist (linear search) --add a value to a table if it doesn't already exist (linear search)
--returns true if the value was added, else false --returns true if the value was added, else false
function _table.add_value(t, a) function tablex.add_value(t, a)
local i = _table.index_of(t, a) local i = tablex.index_of(t, a)
if not i then if not i then
table.insert(t, a) table.insert(t, a)
return true return true
@ -88,7 +81,7 @@ local function _random(min, max, r)
end end
--pick a random value from a table (or nil if it's empty) --pick a random value from a table (or nil if it's empty)
function _table.pick_random(t, r) function tablex.pick_random(t, r)
if #t == 0 then if #t == 0 then
return nil return nil
end end
@ -96,7 +89,7 @@ function _table.pick_random(t, r)
end end
--shuffle the order of a table --shuffle the order of a table
function _table.shuffle(t, r) function tablex.shuffle(t, r)
for i = 1, #t do for i = 1, #t do
local j = _random(1, #t, r) local j = _random(1, #t, r)
t[i], t[j] = t[j], t[i] t[i], t[j] = t[j], t[i]
@ -105,7 +98,7 @@ function _table.shuffle(t, r)
end end
--reverse the order of a table --reverse the order of a table
function _table.reverse(t) function tablex.reverse(t)
for i = 1, #t / 2 do for i = 1, #t / 2 do
local j = #t - i + 1 local j = #t - i + 1
t[i], t[j] = t[j], t[i] t[i], t[j] = t[j], t[i]
@ -116,7 +109,7 @@ end
--collect all keys of a table into a sequential table --collect all keys of a table into a sequential table
--(useful if you need to iterate non-changing keys often and want an nyi tradeoff; --(useful if you need to iterate non-changing keys often and want an nyi tradeoff;
-- this call will be slow but then following iterations can use ipairs) -- this call will be slow but then following iterations can use ipairs)
function _table.keys(t) function tablex.keys(t)
local r = {} local r = {}
for k,v in pairs(t) do for k,v in pairs(t) do
table.insert(r, k) table.insert(r, k)
@ -126,7 +119,7 @@ end
--collect all values of a keyed table into a sequential table --collect all values of a keyed table into a sequential table
--(shallow copy if it's already sequential) --(shallow copy if it's already sequential)
function _table.values(t) function tablex.values(t)
local r = {} local r = {}
for k,v in pairs(t) do for k,v in pairs(t) do
table.insert(r, v) table.insert(r, v)
@ -135,15 +128,15 @@ function _table.values(t)
end end
--(might already exist depending on luajit) --(might already exist depending on luajit)
if _table.clear == nil then if tablex.clear == nil then
if _table ~= table and table.clear then if tablex ~= table and table.clear then
--import from global if it exists --import from global if it exists
_table.clear = table.clear tablex.clear = table.clear
else else
--remove all values from a table --remove all values from a table
--useful when multiple references are floating around --useful when multiple references are floating around
--so you cannot just pop a new table out of nowhere --so you cannot just pop a new table out of nowhere
function _table.clear(t) function tablex.clear(t)
assert(type(t) == "table", "table.clear - argument 't' must be a table") assert(type(t) == "table", "table.clear - argument 't' must be a table")
local k = next(t) local k = next(t)
while k ~= nil do while k ~= nil do
@ -175,19 +168,19 @@ end
-- if into specified, copies into that table -- if into specified, copies into that table
-- but doesn't clear anything out -- but doesn't clear anything out
-- (useful for deep overlays and avoiding garbage) -- (useful for deep overlays and avoiding garbage)
function _table.copy(t, deep_or_into) function tablex.copy(t, deep_or_into)
assert(type(t) == "table", "table.copy - argument 't' must be a table") assert(type(t) == "table", "table.copy - argument 't' must be a table")
local is_bool = type(deep_or_into) == "boolean" local is_bool = type(deep_or_into) == "boolean"
local is_table = type(deep_or_into) == "table" local istablex = type(deep_or_into) == "table"
local deep = (is_bool and deep_or_into) or is_table local deep = (is_bool and deep_or_into) or istablex
local into = is_table and deep_or_into or {} local into = istablex and deep_or_into or {}
for k,v in pairs(t) do for k,v in pairs(t) do
if deep and type(v) == "table" then if deep and type(v) == "table" then
if type(v.copy) == "function" then if type(v.copy) == "function" then
v = v:copy() v = v:copy()
else else
v = _table.copy(v, deep) v = tablex.copy(v, deep)
end end
end end
into[k] = v into[k] = v
@ -196,7 +189,7 @@ function _table.copy(t, deep_or_into)
end end
--overlay one table directly onto another, shallow only --overlay one table directly onto another, shallow only
function _table.overlay(to, from) function tablex.overlay(to, from)
assert(type(to) == "table", "table.overlay - argument 'to' must be a table") assert(type(to) == "table", "table.overlay - argument 'to' must be a table")
assert(type(from) == "table", "table.overlay - argument 'from' must be a table") assert(type(from) == "table", "table.overlay - argument 'from' must be a table")
for k,v in pairs(from) do for k,v in pairs(from) do
@ -210,30 +203,32 @@ end
--note: you can use a larger unpack than you need as the rest --note: you can use a larger unpack than you need as the rest
-- can be discarded, but it "feels dirty" :) -- can be discarded, but it "feels dirty" :)
function _table.unpack2(t) function tablex.unpack2(t)
return t[1], t[2] return t[1], t[2]
end end
function _table.unpack3(t) function tablex.unpack3(t)
return t[1], t[2], t[3] return t[1], t[2], t[3]
end end
function _table.unpack4(t) function tablex.unpack4(t)
return t[1], t[2], t[3], t[4] return t[1], t[2], t[3], t[4]
end end
function _table.unpack5(t) function tablex.unpack5(t)
return t[1], t[2], t[3], t[4], t[5] return t[1], t[2], t[3], t[4], t[5]
end end
function _table.unpack6(t) function tablex.unpack6(t)
return t[1], t[2], t[3], t[4], t[5], t[6] return t[1], t[2], t[3], t[4], t[5], t[6]
end end
function _table.unpack7(t) function tablex.unpack7(t)
return t[1], t[2], t[3], t[4], t[5], t[6], t[7] return t[1], t[2], t[3], t[4], t[5], t[6], t[7]
end end
function _table.unpack8(t) function tablex.unpack8(t)
return t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8] return t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8]
end end
return tablex

View File

@ -2,24 +2,13 @@
2d vector type 2d vector type
]] ]]
--[[ local path = (...):gsub("vec2", "")
notes: local class = require(path .. "class")
local math = require(path .. "mathx") --shadow global math module
some methods depend on math library extensions local vec2 = class()
math.clamp(v, min, max) - return v clamped between min and max
math.round(v) - round v downwards if fractional part is < 0.5
]]
local vec2 = {}
vec2.type = "vec2" vec2.type = "vec2"
--class
vec2._mt = {__index = vec2}
function vec2:init(t)
return setmetatable(t, self._mt)
end
--probably-too-flexible ctor --probably-too-flexible ctor
function vec2:new(x, y) function vec2:new(x, y)
if x and y then if x and y then

View File

@ -2,33 +2,15 @@
3d vector type 3d vector type
]] ]]
--[[
notes:
some methods depend on math library extensions
math.clamp(v, min, max) - return v clamped between min and max
math.round(v) - round v downwards if fractional part is < 0.5
]]
--import vec2 if not defined globally --import vec2 if not defined globally
local global_vec2 = vec2 local path = (...):gsub("vec3", "")
local vec2 = global_vec2 local class = require(path .. "class")
if not vec2 then local vec2 = require(path .. "vec2")
local path = ... local math = require(path .. "mathx") --shadow global math module
local vec2_path = path:sub(1, path:len() - 1) .. "2"
vec2 = require(vec2_path)
end
local vec3 = {} local vec3 = class()
vec3.type = "vec3" vec3.type = "vec3"
--class
vec3._mt = {__index = vec3}
function vec3:init(t)
return setmetatable(t, self._mt)
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 x and y and z then