lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


On 06/06/2017 12:38 AM, Sean Conner wrote:
[1]	I may allow such use for single use functions for modules, but
then again, I do modules a bit differently [2] to maintain easy
compatability with Lua 5.1.

[2]	Check out
https://github.com/spc476/lua-conmanorg/blob/master/lua/date.lua for
 an example.

On modules / globals:  This is an important aspect of code style &
organization that has a big impact on how you can interact with the code
and is hard to change later.  Most common styles rely heavily on
`local`s / upvalues, which (IMHO) negatively impacts debuggability &
flexibility.

As an example, let's say you have public `foo.unparse`, which uses
(among many others) internal `readDateTime` which uses internal
`readTime` (which happens to contain an arithmetic bug – it returns
slightly wrong values of the right type, so there's no nice stack
trace).  Your tests tell you that `unparse` gives wrong results.  Among
the dozens of internal functions, how do you localize the bug?

To test the pieces – the internal functions – (whether by test scripts
or in the REPL) you can (temporarily) edit the source and either put
your test code in there (so no REPL for you) or remove the `local`
annotations (yuck!), or you traverse the chain of upvalues.  If you add
metatable magic on functions' `__index`, this can be as "nice" as
`foo.unparse.readDateTime.readTime`, otherwise it may be as terrible as
select(2,debug.getupvalue(select(2,debug.getupvalue(foo.unparse,1)),2)).
Both are brittle: Code changes may change the path you have to take, you
often have to traverse several levels of upvalues, …  I can't think of a
better way to access the parts (and I don't think there is one…)

How do people using these module styles deal with that?  Is it actually
a problem or are you (e.g.) using a completely different debugging
approach?  (How/) Do you test internal functions?  (How/) Do you REPL?



I try to avoid the problem by giving each module its own module
environment (separate from the module table), usually starting modules
with (5.1-5.3 compatible)

  local _ENV = setmetatable( { _M = { } }, { __index = _ENV or _G } )
  if setfenv then  setfenv( 1, _ENV )  end
  _M._MENV, package.loaded[...] = _ENV, _M

(_M is the module table, expose stuff by adding to _M.  _ENV / _M._MENV
is the module environment and __index-es into the parent environment.
Don't `local` by default, put stuff in the module environment.  You can
additionally `local` for speed where needed, as in `foo = …; local foo =
foo` or `local foo = …; _ENV.foo = foo`.)

This avoids both the problem of accidental non-`local`s becoming global
or public and makes the environment easily accessible for debugging.
(If you prefer, omit the `_M._MENV = _ENV` and `debug.getupvalue` the
environment (once, from (almost) any exposed function) for debugging.)

(Manually setting `package.loaded[modname]` is another thing that I see
rarely.  If you do this, you don't have to return the module at the end
– which means all module management happens in one place (at the top),
not two (top and bottom).  It also permits module "foo" to `require
"foo.bar"`, which can then `require "foo"`, without spinning in a
require-loop.)

Another nice property is that submodules can share their environment –
e.g. in "foo.bar" start instead with `local _ENV = require "foo"._MENV`.
(This is really nice for "util" modules – they can simply populate the
module environment and don't have to add a contrived dummy package.
It's also much easier to split a single module's implementation across
several files.  Etc. pp. – with this style/approach, the module
environment is not just a bunch of scattered upvalues, it's an actual
environment/table that can easily be queried/manipulated/…)

-- nobody