Is my first lua module lua-ish? Plus questions...

74 views
Skip to first unread message

Mark

unread,
Jan 8, 2025, 7:02:25 AM1/8/25
to lua-l
I have created a couple of lua modules here:

Str.lua adds some methods to string; it is mostly copied from other people with a couple of my own ideas.

Set.lua is the module I'm concerned with. I'm not too bothered about efficiency, but my questions here are:
- Is the module in good lua style?
- I want insersect to be a function and the & operator; similarly for union. I have this working, but am I doing it the best way? (See Set:new)
- Is there a better (faster) way to get a random element?
- Similarly for the iter() iterator?

I have tests in test_Set.lua. This uses the check.lua module.

In check.lua I have two local module-level variables, ok and total. This module is used by both test_Str.lua and test_Set.lua and in both cases the variables are correctly initialized to 0: but I do *not* understand why! I thought each module was loaded only once? (I'm very happy with the way it works, but would like to know _why_ it works this way.)

Also, is there a nicer (shorter) way to set the check.lua message to OK or FAIL?


Francisco Olarte

unread,
Jan 8, 2025, 8:03:16 AM1/8/25
to lu...@googlegroups.com
Mark:

On Wed, 8 Jan 2025 at 13:02, Mark <m.n.sum...@gmail.com> wrote:
> I have created a couple of lua modules here:
> https://github.com/mark-summerfield/lx

> Str.lua adds some methods to string; it is mostly copied from other people with a couple of my own ideas.
Only problem would be if you use a similar but not equal module and
you stomp around each other in string.

> Set.lua is the module I'm concerned with. I'm not too bothered about efficiency, but my questions here are:
> - Is the module in good lua style?

I think you are messing around too much with the metatables. In
Set:new you asume you are being called with colon notation and
(idempotently, if correctly called ) rebuild the metatable each time.
Normally it is better to build it just once and IMO, not use "method
call" syntax for new. And make the metatable private.

If you do
-- Confine the methods to a table:
local set_methods = {}
function set_methods:add(...)......
-- Now lets go for the metatable:
local set_meta = {
__index = set_methods, -- This will send method calls to the correct point..
}
function set_meta:__len().,... -- This will make # work.
-- Although I would prefer to have a method too, not a fan of
excessive operator overriding, that can be done like this:
function set_methods:len() -- Build the method.
set_meta.len = set_methods.len --
-- Rest can be deducted, now the basic function to build a Set would be:
local function build_Set(...)
-- Done in steps:
local self = { items_ = {}, size_ = 0 } -- We are not in a method
but this simplifies reading.
setmetatable(self, set_meta)
self:add(...)
return self
end
-- For this kind of things I normally make the add method chainable (
return self ), which has among other things the benefit of
function build_Set_with_chainable_add(...)
return setmetatable( { items_ = {}, size_ = 0 } , set_meta):add(...)
end
Anyway, the thing is how to make this work as a class. One easy way is
to end your module with:

return build_set;

-- This will allow you to do this in users code:
local Set = require "Lib/Set"
local s = Set(1,2,3)
-- Or, if you add more classes you can do:
return {
Set = build_set,
Bag = build_bag,
}

and something like DS=require"data_structs"; my_set = DS.Set()

But this comes with practice, the big thing is to 1.- build your
methods and metatables first, not only to avoid rebuilds, but to keep
it tidier, and 2.- remember "constructors" in a basic object dessign
are just functions, so no : normally. Of course you can go in a way
that makes classes objects and constructors methods on them, but this
is just a kind of factory pattern and has its own chicken and egg
problem.

> - I want insersect to be a function and the & operator; similarly for union. I have this working, but am I doing it the best way? (See Set:new)

Note your set.new nearlyu assumess Set==self ( my build_set show a way
to skip that ).
For the intersection trick, do the same I showed for len()/__len(),
nearly equal to what you did.
In fact given intersection returns a new Set I normally prefer:

local function set_intersect(set1,set2)..... return new_set end
set_methods.intersect = set_intersect
set_meta.band = set_intersect ( or even set_methods.intersect for more
complex cases, for future proofing ).

> - Is there a better (faster) way to get a random element?

Difficult to do with the way you store it, but if you use it a lot you
can dessign around.

> - Similarly for the iter() iterator?

Sorted is not your problem there. I see among other things you seem to
be relying on only having strings but not checking that. That may be a
harder problem.

You may be able to build the array a bit faster by directly calling
next, something like:
local sitems = self.items
local items = {}
local item = nil
for i=1,math.maxinteger do
item = next(sitems, item)
if item == nil then break end
items[i] = item
end


Or even something like:
local sitems = self.items
local items = {}
local item = next(sitem)
local i=1
while (item ~= nil) do
items[i] = item
i=i+1
item = next(sitems, item)
end

But as always, measure twice cut once.

I'll look at your check questions after lunch :)

Francisco Olarte.

Francisco Olarte

unread,
Jan 8, 2025, 9:33:31 AM1/8/25
to lu...@googlegroups.com
Mark:

On Wed, 8 Jan 2025 at 13:02, Mark <m.n.sum...@gmail.com> wrote:
...
> In check.lua I have two local module-level variables, ok and total. This module is used by both test_Str.lua and test_Set.lua and in both cases the variables are correctly initialized to 0: but I do *not* understand why! I thought each module was loaded only once? (I'm very happy with the way it works, but would like to know _why_ it works this way.)

If you are using st.sh to run the tests you are invoking lua twice, ok
and total are set once in each. Try invoking them from the same lua
invocation and you'll see it only gets set once.

> Also, is there a nicer (shorter) way to set the check.lua message to OK or FAIL?

Unless you define the total problem a bit better, it is difficult to
answer this. Some obvious patches, like having a file-level local
message initialized to "OK" and setting it to "FAIL" in the else
branch of check() are not probably what you totally want.

Francisco Olarte.

Mark

unread,
Jan 9, 2025, 5:53:50 AM1/9/25
to lua-l
Franciso,

Thanks for your help!

I renamed Str.lua and Set.lua to be all lowercase and for str.lua I created the Str module with functions instead of adding them as string methods.

I changed check.lua so that it has a single function, checker(), which is a closure and factory that returns two functions (check() and report()) both of which used captured ok and total variables.

I also got the idiom for the message: local message = ok == total and "OK" or "FAIL"

The most work was trying to change set.lua to follow the approach you recommended.

I've now done this with a methods table and a meta table and a single (now much simpler) Set function (which you called build_set) which I return.

I can now use str.lua as a model for further collections of functions and set.lua as a model for classes.
Reply all
Reply to author
Forward
0 new messages