Optional chaining operator

As a Roblox developer, it is currently too hard to access a property of something that is deeply nested.

Currently, you have to check if the parent of the property you’re trying to access is nil. If you don’t, you have a risk of getting a nil error.

TypeScript has an awesome feature called the optional chaining operator, which you can already use in Roblox-TS.

The operator would work pretty much the same as the JavaScript/TypeScript one, with the only addition being the ability to use it with a colon when calling a function.

An example:

-- Old code
if something and something.someProperty then
	something.someProperty:SomeFunction()
end

-- New code
something?.someProperty?:SomeFunction()

If this issue is addressed, it would improve my development experience because it would be way more concise and readable to access something deeply nested.

67 Likes

I usually use:

local thing = (bool and 1 or 2)

Which would store 1 in thing if the boolean was true, or 2 if the boolean was false. I like to chain these as well. You can do things like

local position = (part and part.Position or ...)

or

local propertyValue = ((something and something.someProperty) and something.someProperty or nil)

6 Likes

It’s still much more readable to write part?.Position than (part and part.Position).

6 Likes

That’s nice, but inefficient and ugly in case of deeply nested hierarchies. For every step you have to reindex all the previous values. For example, the following code would require 6 table lookups.

local health = tabPlayerData[plyrname] and tabPlayerData[plyrname].Stats and tabPlayerData[plyrname].Stats.Health

The performance issue can be fixed with a better compiler, but nobody wants a 100 character one liner that just fetches a single value.

8 Likes

Yes! And (although it would be a much more niche usage) a null coalescing operator would be nice too.

4 Likes

Should this also apply to non-table values that doesn’t have __index? Should x?[y] also be added?
I’d include __optionalindex metafield along with it, but I don’t think everyone would agree with me and understandably so.


Although this one has less use cases and is more niche, let’s take this even further and add ?(, ?+, ?-, ?*, ?/, ?%, ?^ and ?#x, where the right hand side of the operator only evaluate if the left hand side isn’t nil and if either one of those is nil it’ll evaluate to nil.

For example:

local myfunc = math.abs
print(myfunc?(-5)) --> 5
myfunc = nil
-- The arguments are not evaluated
print(myfunc?(-5)) --> nil
5 Likes

With a functionality including what @Blockzez said would be great, which I have a proposal for below, and I’d honestly be all for something like this.

Also, what @Neutron_Flow suggested can easily be done like so (which is safe for the none type too):
abc ~= false and abc

If abc equals false, the result will be false and the expression ends, if abc is nil, none, or anything else, the result will be simply abc.

With what @Blockzez said, I don’t think a new metatable is a good idea, I think it makes most sense if the operator behaves the exact same as indexing, it just greedily “cancels” the expression. (Keep in mind, JavaScript actually also has a metatable equivalent, Proxy() which doesn’t have any different effects from the chaining operator vs normal indexing afaik)

I think it makes sense logically that this chaining effectively resolves to a none type (rather than nil) if any of the full expression fails. (abc?).cde vs (abc)?.cde would error on the first one and the second one would behave equivalently to abc?.cde. This would not cause any issues, as you can simply do something like the following if you happen to need nested behaviour like that: (abc?.cde or efg)?.ghi.

As for my reasoning why it should be the none type instead of the nil type, converting it to a nil type can be confusing in the case of tuples, and since <none>? would resolve to nil based on what others are saying.

And, of course, since this is still normal indexing, game.IDontExist? will still cause an index error for an invalid property rather than resolving to none which I would expect, the only difference here is that when the optional result is nil or none the entire expression resolves to nil or none.

In this case when I say expressions by the way, nil? or abc() has two “expressions” on the left and right, and content within parentheses are also expressions:(abc) or (cde or (efg))

The following would then easily be possible without changing behaviour (and can be represented in lua already):

someFunction?(123, value?)?

You can break this expression down into a “universal” equivalent like so (which would be pointlessly slow, of course):

(function()
	if someFunction then
		-- Yeah, this isn't correct syntax, so, you'll have to 1v1 me to be allowed to be upset
		local result, ... = (someFunction(123, (function()
			if value then
				return value
			end
		end)()))

		return (function()
			if result then
				return result
			end
		end)(), ...
	end
end)()

Funnily enough, I use behaviour like this already in my code, though, when possible I always use something like abc and abc.cde or nil.

This would actually allow us to freely create the none type as well, which would be really cool for testing purposes since nil and none can be differentiated between and often are in C code. For example, some Roblox functions expect something non none, such as nil. A really good example is modules, modules expect one single value type to be returned that is non none.

You would expect that this would error:

-- Module that errors

return nil?

But this would not:

-- Module that works

return nil

You can actually test this like so:

-- Will error since the returned type is none, not nil or anything else
return (function()
	return -- This is actually redundant, just added it for demonstration purposes
end)()

Another example of this is console output:

print(nil?) -- (nothing)
print(nil) -- "nil"

And, of course, if you really needed a non none type this honestly isn’t so bad:

abc? or nil
6 Likes

There are not “tuples”; Multiple returns aren’t packed into a single data structure, if Lua returns multiple values they return multiple value, not a single data structure called tuples; neither are no value a “none type”. The way you phrased it implies that they’re separate data types/structures instead of the being how many value they returned.

I believe that in Lua, no value means no value, I don’t think Roblox needs a special effect for having no values nor a special syntax for manipulating multiple values. print(1, 2, nil?, 3) would it evaluate to 1, 2, 3 or 1, 2, nil, 3 or would it be a syntax error?

Go and Common Lisp have a similar concept to Lua’s tuple multiple return so should this also apply to Go and Common Lisp?

1 Like

You’re technically correct, yes.

The devhub refers to functions which return multiple values as returning tuples.
image
According to tutorialspoint.com:

“Multiple values” very much so are tuples C-wise, you just don’t interact with them as actual tuple objects in lua. You can still use it just like a tuple though:
select("#", ...) returns the tuple size
select(n, ...) returns the nth element in the tuple

Personally, I refer to them as tuples as I’m pretty sure that’s the proper term and its the one that makes most sense to me. Plus, “multiple values” is just harder to say.

Additionally, from the lua source code, none does have a type identifier separate from lua while it doesn’t work quite the same as other types since you can’t have actual none values, you’re technically correct, but again, its easier to call it “none” than “no value” or “an empty tuple”:

#define LUA_TNONE       (-1)
#define LUA_TNIL        0

You can’t keep a value with that type, it is essentially just an empty tuple, but, in C code you can actually have a real “none value” that’s also not nil, and can return it from C functions as well.

And, here’s a way to get a void type to test what you were asking: print(1, 2, (function()end)(), 3) would print 1 2 nil 3, because there is no way to have a tuple inside a tuple (hence why you can’t do much with it)

By the way, here’s a neat trick I used to use before table.pack and table.unpack to store tuples from variadic functions (actually have this in an older utility I released on here):

local function tuple(...)
    local thread = coroutine.create(function(...)
        coroutine.yield()
        return ...
    end)
    coroutine.resume(thread, ...)
    return func
end

local tup = tuple("a", "b", "c")

print(tup())

C-wise I believe that its uses the stack to keep track of multiple value in Lua but feel free to correct me as I might be wrong. It looks like I’m implying that none ~= no value
but I did not explicitly say this, what I’m trying to say is that no values/none aren’t a data type in Lua nor multiple values are a data type.

Do you have any formal (vanilla Lua) documentation that refers multiple values as tuple? I don’t trust DevHub outside of Roblox’s API. I checked the Lua 5.1’s manual and no mention of tuple whatsoever, they just refer it as “mutliple values”.

Using multiple values doesn’t create a “tuple” object.

It returns everything after the nth element, not just one value.

LUA_TNONE isn’t the type of any actual values, you can’t push a “none” value.
Lua 5.4 Reference Manual

For functions that can be called with acceptable indices, any non-valid index is treated as if it contains a value of a virtual type LUA_TNONE , which behaves like a nil value.

Lua will only use all results if the call is the last expression in a list of expressions and if the call isn’t wrapped in parenthesis. The reason this happens isn’t because of tuples, but because Lua uses a register based virtual machine. For the number 3 to be added to the argument list it must be loaded at a specific register, which means that the previous call must have a fixed number of results. If it created a tuple object, then it should be able to just move everything into the new tuple.

Did you forget to define func?
This could be done using tables, even without table.pack and table.unpack.

local function tuple(...)
    local args = {...}
    local argn = select("#",...)
    return function()
        return unpack(args,1,argn)
    end
end
5 Likes

This would be useful! Been using roblox-ts lately, and this operator has been helpful for the reasons mentioned above. While we’re at it, the type assertion operator ! would be helpful too. That way when you can be sure something is there while linter/compiler might say otherwise, you don’t have to do (x :: y).z anymore, you can just do x!.y

4 Likes