Non-nil (or non-anything) values?

So I have a confusion about this problem:
It seems the only way to do this is by using a function or unpack:

First of all, when a function returns multiple values:
local function f()
return 1, 2, 3, 4, 5
end
local a, b, c, d, e = f()
They all have their respective numbers.> a=1, b=2…e=5
I can change that into a table:
local a = {f()}
They all have their respective indexes.> a[1]=1, a[2]=2…a[5]=5
BUT
When I try getting the type of this:
type(f())
It seems to get the type of the first value, which is 1
Now that says something. This also seems to be related with “nothing”
local function f() end
type(f()) --ERROR (also the same as type() without any arguments)
As opposed to nil (which ironically means nothing)
type(nil) -- "nil"
I feel like what is essentially happening here is that unpack({a, b, c}) acts more like flat out “a, b, c” that you can drag and drop into the code.

Still the question is: What is this mysterious value that acts like a table, that I feel like I can hold in my hand, but still can’t.

The reason type(f()) in your example only returns the type of the first value is because it only cares about the first value. What you’re doing is the same as type(1, 2, 3, 4, 5). It doesn’t expect more than 1 argument so it ignores the rest.

As to why type() isn’t the same as type(nil), C can actually tell the difference between no value and nil value, because nil is actually something for C, and nothing for lua.

2 Likes

I think you would call the return of multiple values from something a tuple, if you want to give it a name. Tuples are really just sets of values, and it’s impossible to capture it as a singular value except via table construction or a varargs argument to a function, function f(...)

There’s also another way to play with tuples, which is mainly through the built-in select function, which receives a number as the first parameter, and returns that respective value from the tuple passed in as the second parameter, select(n, ...), along with all the values after it apparently. There’s also a special length argument you can pass in instead of an index as "#", which gives the number of arguments in the tuple.

I knew there was something to do with C or something. I guess it’s implemented as an actual thing, but just not listed in the documentation or is it?

I’ve heard of the select function, but I’ve never actually seen a use for it, I mean, it’s so specific. Also heard of tuple, guess it is a thing after all?

It’s true that there aren’t many use cases for tuples, I would mainly use them to help save on space:

local function f()
	return 1, 2, 3, 4, 5
end

-- instead of
local _, _, _, four = f()

-- you can do
local four = select(4, f())

But you can also use them to preserve length in arrays, since having nil as one of the values could lead to weird behavior with the length operator.

local function f(...)
	local argsLen = #{...}
	local realArgsLen = select("#", ...)
	print(argsLen, realArgsLen)
end

f(1, 2, 3, nil) --> 3 4

Even then, table.pack takes care of this for you with an extra n field:

local function f(...)
	local argsLen = #{...}
	local packArgsLen = table.pack(...).n
	print(argsLen, packArgsLen)
end

f(1, 2, 3, nil) --> 3 4

The reason why this would be important though is if nil is somewhere in the middle of the array and you decide to add a new value; its length would change to the highest consecutive non-nil value:

local array = {1, 2, 3, nil, 5}
print(#array) --> 5

array[6] = 6

print(#array) --> 3

Whereas using table.pack helps you preserve length with a custom field:

local array = table.pack(1, 2, 3, nil, 5)

print(array.n) --> 5

array[6] = 6
array.n = array.n + 1

print(array.n) --> 6, obviously
6 Likes

Ok so here’s the lengthy technical explanation. The non-nil, non-existent value is actually just a constant nil value. Lua has an internal stack which is used when interfacing with C and its own state (but on a smaller scale). What you’re seeing is the function type receiving the NONE type when you don’t pass a parameter.

When Lua passes 3 parameters, for example, the stack is pushed up 4 slots (1 for the function, 1 for each parameter). The type function accesses the stack at base+1, which would be the first parameter when calling the function.

If you only push the function, then the value at base+1 does not exist because the stack was never grown to that or the top wasn’t set to it. Lua calls a function called index2addr which is in charge of resolving what value is at a stack location. This function realizes (if (o >= L->top) return NONVALIDVALUE;) that if we’re accessing a value outside the top of the stack, it probably doesn’t exist and is unsafe, so a constant NONVALIDVALUE nil is returned.

Now on to what type does when it sees this value. The internal lua_type function (not the one in the global env) treats this as a NONE value instead of NIL. The type function, which gets its result from lua_type, has a check in place that makes it throw an error when it receives NONE, which is the one you’re seeing.

3 Likes