Why not "t[!]" or "t[~]" to refer to the metatable with the index notation (except that the index is not valid _expression_ here, it's a syntaxic construct)
If you want a member notation (with ".", or with ":" in function calls) this would be more problematic.
The alternative would be to give a meaning to t[nil]: trying to read index nil normally generates an exception (with the default getindex accessor) but could as well allow reading/writing the metatable member itself. But it could create caveats (like t[a][b][c] in sparse arrays where an assigment would in fact modify unrelated metatables for invalid index positions)
But another notation that would be useful is the access to members of the metatable (only using getmetatable):
- t[['m']] would access to the member 'm' of the metatable of t as a shortcut to getmetatable(t)['m']
- t[[[m']]]
would access to the member 'm'
of the metatable of the metatable of t as a shortcut to
getmetatable(getmetatable(t))['m']
- and so on...
These would be valid in _expression_ as rvalues (reading these member) and as lvalue (if needed, the assignment will assign an empty table as the metatable if there's none, but this won't happen for most objects notably tables, userdata, functions and coroutines/threads, except for numbers or strings that would be transformed into objects with default get/set accessors members and a hidden __value member; but if this is not allowed, it would generate a typecheck exception; of course "nil[[anything]]" would generate an exception as the nil value (of the builtin type 'nil') cannot have any metatable).
But I agree that get/setmetatable() are not well integrated in the language itself as it's a function of the library. There's also the same problem for fgetenv() and fsetenv(): this may be seen as a "feature" of the language, but it would be preferable if these featured APIs where in fact part of the controled environment that resolve these function calls by name using optional metaentries in like _getindex, __add and so on for other builtin operators (which are not necessary present in the parent environment, so if they are nil, the builtin implementation is used).
--
Another thing that would help create fast cachable lookups in the environment would be to have "const" variables: whose value can only be set once at the definition, but write-protected so that multiple further lookups are not needed and that allows the compiler to optimize the generated code, by finding constant common expressions (evaluated only once, their value being kept eitheir in the constant pool or in the current environment, if needed for some far reuses when the compiler cannot keep it).
With "const" variables, the immediate extension would be to have a syntax for "enum" (for integer constants):
enum{a,b,c}
would be equivalent to
local a,b,c = 1,2,3
or better (if "const" is defined)
And this would be also valid:
enum{ a=10, b, c=1 }
equivalent to:
local a,b,c = 10,11,1 -- or better: const a,b,c = 10,11,1
where it would even be legal to define different constants as "members" of the enum having the same value.
The reason for restricting "enum" declarations to integers is because table lookup by index is efficient ONLY with integer keys, other types being inefficiently hashed and having slow access time with possible collisions; collisions should not occur for any integer index that are part of a sequential range, that tables should optimize for more direct access, without any hashing needed and so without any collision: tables should have two separate stores for the sequential integer indexes and for all other key values; but note that some integer values that fall outside the optimized sequential range may still need hashing and lookups in the hashed part of the table; the sequential part of the table may also contain some additional places outside a normal "sequence" with some holes set to nil, and possibly even integer indexes with negative or null values that fit in the "compact" range of the unhashed part of the table, if this "optimized" part not requiring hashing has two internal members: minindex and maxindex, or
minindex and size, plus a member counting slots in that range that have a nil value, so that the implementation can decide when to reallocate the storage to move integer keys between the sequential and hashed parts of the table, possibly with some additional tuning parameters of the table which may be also changed as desired, such as the min/max fill factors, the minimum size and growth factor, i.e. the same tuning parameters that can also apply to the hashed part of the table for usage of its allocated nodes/buckets and can be easily represented with only 4 bytes in the table header, to control how often and how much will occur the allocations/reallocations/reindexing for growth or reduction of sizes, and so to limit the cost overhead for the garbage collector and still allow reduction of the global memory footprint of the Lua VM).
The limitation of enum declarations with integers (being only those really optimized) is that the assigned constants have a default value which is autoincremented (the first declared member having a default assigned value 1); but if needed we should be allowed to include in the enum any other constant declarations for other types, even if it's not necessary: if the assigned value is a number, its value is rounded down as the last assigned value for the declaration of next constants, otherwise it does not affect the next declared members without assigned value:
enum { pi = 3.14, q, r = -1.01, s = 'sample', t }
would then be the same as:
const pi, q, r, s, t = 3.14, 4, -2, 'sample', -1
(note here the use of "const" instead of "local" for declaring readonly variables)
--
However I wonder if constants declared in enums should not be "typeable" with some extra function indicating their constructors:
enum fun { a,b,c, d = 10, e = 'sample', f = nil }
would be the same as:
const a,b,c,d,e,f =
fun(1),
fun(2),
fun(10),
fun('sample'), fun(nil)
where "fun" is any function in scope defining a constructor and returning a typed object whose reference would be kept in the declared constant. The absence of this function name after "enum" means that the default function is the identity function returning its first parameter. And:
enum fun { a,b,c, d = 10, e = 'sample', f = nil } [12, 'init']
would be the same as:
const a,b,c,d,e,f =
fun(1,
12, 'init'),
fun(2,
12, 'init'),
fun(10,
12, 'init'),
fun('sample', 12, 'init'), fun(nil, 12, 'init'
)
where the constructor function can also receive some additional parameters, in addition to the value declared (or implicitly incremented) in the enum.
Optionally, the constructor function may also be declared at the same time:
enum {a,b,c, d = 10, e = 'sample', f = nil } (...){ return {...} }
[
12, 'init' ]
would be the same as:
const constructor = function (...) { return {...} }
const a,b,c,d,e,f =
constructor
(1,
12, 'init'),
constructor
(2,
12, 'init'),
constructor
(10,
12, 'init'),
constructor
('sample', 12, 'init'),
constructor
(nil, 12, 'init'
)
(except the "constructor" here is an anymous constant function because we did not give any name after "enum"; here "..." is a vararg parameter list of the constructor and is used to create a table for the effective value of each declared constant variable, whose index [1] constains the value "assigned" by inside the enum{...} body, and [2] = 12, [3] = 'init' as in the parameter list)
As well you can name that constructor explicitly as a declared constant:
enum fun {a,b,c, d = 10, e = 'sample', f = nil } (...){ return {...} } [ 12, 'init' ]
would be the same as:
const fun =
function fun(...) { return {...} }
const a,b,c,d,e,f , fun(1,
12, 'init'),
fun (2,
12, 'init'),
fun(10,
12, 'init'),
fun('sample', 12, 'init'),
fun(nil, 12, 'init'
)
Note that because this constructor function is declared as a constant by the enum (because the enum {...} is followed by a parameter list in parentheses and a function body), its code can be inlined by the compiler as if it was:
const fun =
function fun(...) { return {...} }
const a,b,c,d,e,f =
{1,
12, 'init'},
{2,
12, 'init'},
{10,
12, 'init'},
{'sample', 12, 'init'},
{nil, 12, 'init'}
So the "enum" keyword is also a function declarator (just like the "function" keyword, with the same capabilities as other functions) in addition to a declarator for constant variables with initializers assisted by the syntax.
But you can do the almost same inlining by just saying:
const function fun(...) { return {...} }
// ...
enum fun { a,b,c, d = 10, e = 'sample', f = nil } [ 12, 'init' ]
which would be equivalent (but where the constructor function must first be declared and accessible in the current lexical scope by its name "fun" and is not necessarily a constant function; if "fun" is not constant, its code cannot be inlined by the compiler as the effective function reference may change during execution, including during calls of successive initializers).