[Date Prev][Date Next][Thread Prev][Thread Next]
[Date Index]
[Thread Index]
- Subject: Re: Deprecate "Attempt to index a nil value" error; instead, return nil or create table
- From: nobody <nobody+lua-list@...>
- Date: Sun, 1 Mar 2020 23:04:33 +0100
On 01/03/2020 16.46, Philippe Verdy wrote:
Or may be there's a need to subclass the "nil" value: reading an
unassigned position in a table with this property could return this
special "niltable" value (whose type/class would be the same as nil)
but which differs from "nil" by the fact that "niltable" is indexable
like a table but will not throw an error when subindexing it for
further reads or writes and would behave as a no-op.
Basically "niltable" and "nil" would still be equal for "==" but
distinct for identity, and they would both by of type "nil":
nil==niltable and typeof(nil)=="nil" and typeof(niltable)=="nil".
And unlike "nil", "niltable" would have a metatable containing the
array-indexing meta-operator functions. "niltable" would not be
modifiable/mutable at all.
A table that must not throw an error, and "niltable" itself, would
just both have to contain the same meta-operators to override the
default error-throwing behavior.
I've made similar things in the past. By changing __eq to not care
about "primitive types" (number, table, string, …)[1], you can build
"logical types" that are spread across multiple primitive types and
still compare correctly according to the "logical type"'s rules (e.g. a
Complex type that logically contains tables (with .re and .im), plain
numbers (just the real part) and userdata (C complex)[2]; or (here) an
Absent type that contains both plain nil and e.g. tables supposed to act
like nil.) That solves the general problem once, and makes it so that
you don't need a specific nil-like value just for this and then a
foo-like value for something else…
Add __false / __bool and then your fake nil can also be false. (And now
finally falsy values can contain information – so you can store where
this particular nil was generated, and thus you can later use that info
to either generate a fancy error message (bar.lua:42: attempt to add a
nil value, generated at foo.lua:23 when accessing table t at field
"foo"), or to retroactively create fields / intermediate data structure
layers and turn things into a valid assignment. But if you're happy
with fake nils taking the "wrong" (truthy) `if` branch, we don't even
need that…)
type() can be wrapped / monkey patched from inside the language, and
past that point it's fairly hard to keep them apart. (generic `for`
loops and tables[3] will still be able to tell which one's which, but
that's about it.)
-- nobody
[1] More specifically, it's back to the 5.2(?) behavior where a == b
calls the metamethod only if both values have the same __eq metamethod,
which prevents accidental asymmetric garbage like
tt = setmetatable( {}, { __eq = function() return true end } )
ff = setmetatable( {}, { __eq = function() return false end } )
print( tt == ff, ff == tt )
--> true, false
(you could still do __eq = function() return math.random( 2 ) == 1 end
or explicitly share the metamethod and add logic that creates asymmetry)
and also prevents logically illegal comparisons (so your __eq doesn't
have to also do a type check, beyond what it itself needs) and then
removing the constraint that primitive types have to match… at that
point, it's just
Nil = setmetatable( {}, { __eq = function() return true end } )
debug.setmetatable( nil, getmetatable( Nil ) )
and primitive equality of the __eq metamethod is what's doing all the
heavy lifting. (Only fake and real nils have this __eq metamethod, so
only comparisons between them will call this __eq. They should all be
logically equal, so that's precisely what the function says.)
[2] If you set __index for numbers to allow (only) .re (returns the
number itself) and .im (returns 0), then it's just
__eq = function( a, b ) return a.re == b.re and a.im == b.im end
no matter how the particular values are represented! (Primitively
comparable values (numbers) still compare according to their rules even
though there's an __eq metamethod, so this doesn't explode.) I think
that's a very nice, terse solution, and it's actually _more_ readable
than the usual type()-case done by nested `if`s…
[3] If you make it so that _ALL_ newly created tables set the value from
the per-type metatable list as their metatable (ordinarily that's nil,
so unless you set that, nothing really changes), you have the means to
prevent that: Set a metatable for new tables in the per-type metatable
list that has __newindex, which checks for fake nils & maps them to real
nils / errors out. Past that point, only generic `for` really cares
about the "real" `nil`. (Further, if you move the per-type metatable
list from the global state to each coroutine (and they're initialized at
creation from the parent coroutine), then each coroutine can do whatever
it needs without stepping on the toes of the others… at which point you
basically have independent "universes" with different local rules. That
can already be done with fully independent Lua states, but then you
can't directly send tables or full userdata across…)