Syntactic Sugar for Assignment Defaults

As a Roblox developer, it is currently too hard to do default assignments in a performant but also compact way.

If Roblox is able to address this issue, it would improve my development experience because it would allow for me to condense enormous sections of code into something much more readable.

With luau a few new shortcuts were added for doing operators and assignments (+=, -=, /=, *=, etc). This greatly reduces the size of some code and makes it way easier to type out some things.

One thing which is used everywhere, maybe just as commonly is default assignments. When the existing value is nil it gets assigned something, but, after that it isn’t assigned anything new.

This is the most performant way I know of to do this:

local value = current
if value == nil then
	value = default
	current = value
end

The problem with this, however, is, it takes up 5 whole lines and is difficult to type even with autocompletion and excessive use of copy pasting. It has very bad readability too, and, I think its a confusing piece of code to look at compared to most things, but, its used almost everywhere on Roblox.

If this got its own assignment operator not only could this likely be used to improve performance by not having to allocate extra locals and do extra assignments, this would greatly improve the readability and size of code and would greatly speed up my development process.

I usually use this simple structure at least two or three times in most scripts and sometimes even more due to how useful it is but that ends up contributing a lot to how much I am typing and it happens to be one of the slowest structures for me to type out since most of it can’t be auto completed and it isn’t repetitive.

Here is an example snippet from some code I was recently working on:

local services = {}
function Linker:GetService(name)
	local serviceModule = ClientServices:FindFirstChild(name)

	if not serviceModule then
		error("Invalid service '"..name.."'", 2)
	end
	
	local service = services[serviceModule]
	if not service then
		service = require(serviceModule)
		services[serviceModule] = service
	end
	return service
end

I use this sort of thing in my frameworks all of the time and it always feels like bloat to me

Having a way to do some sort of “default assignment” would fix a lot of my issues and make this much more clear as well as potentially allowing for extra performance benefits by reducing assignments even more.

Additionally, being able to return and assign at the same time would save an extra value access and condense the code more.

I know syntax proposal is an iffy thing, but, as an example if the above code could become this it would greatly save space and could allow for extra performance benefits:

local services = {}
function Linker:GetService(name)
	local serviceModule = ClientServices:FindFirstChild(name)

	if not serviceModule then
		error("Invalid service '"..name.."'", 2)
	end
	
	return services[serviceModule] := require(serviceModule)
end

This wouldn’t need to mean assignments can act as expressions which introduces a lot of problems, instead it could be its own syntax that can happen exclusively after a return, kind of similar to how you might look at a repeat until loop where you can have an expression after but in this case its any statement, function call, assignment, etc.

return a, b, c, _, _ = 1, 2, 3, 4, 5 could be valid for example and would assign a, b, and c to the corresponding values and then return all 5 values in order which would allow for returning any shape you currently can by using _s as a placeholder like for loops already do.

3 Likes

You could always do this with assertions? You would just have to change how you index instead.

function Linker:GetService(name: string)
    if services[name] then return services[name] end

    services[name] = require(assert(ClientServices:FindFirstChild(name), "Invalid service " .. name))
    return services[name]
end

I don’t understand exactly what you mean here. The issue is not around a module existing or not the issue is around caching things, in general, not just with my specific example.

If I want to cache a result in a table I don’t want to overwrite it every time and I don’t want to be executing code to produce a valid value every time.

The “default assignment” seems quite niche, and := is used in some languages for regular assignment (e.g. Python).

, conflicts with expression lists, it could be reasonably interpreted as only assigning to _

return a,b,c,_,(_=1,2,3,4,5 --[[extra values get ignored]])

or

return a,b,c,_,(_=1),2,3,4,5

Also, _ isn’t exactly a placeholder, it can be used as a variable name

local _ = 1
print(_)
_ = 2
print(_)

= as an expression is problematic, it would get frequently misused when == was intended

local x = 5
if x = 5 then print(x) end

This isn’t niche at all, it gets used all of the time. And, := was just an example, not a syntax proposal, the : is unimportant to the feature itself. Likewise, the syntax itself doesn’t even need to be shaped like an assignment in the first place.

The , wouldn’t conflict with expression lists because the assignment replaces the possibility for an expression list in the first place, and, again, that’s just an example, not a proposal. You’re using assignments as an expression when I said in the post that this is problematic and my example avoids needing to make assignments an expression through a specific scenario. (Like continue/break in loops, and the use of the do keyword being completely different, or how repeat until has a condition on the end that would make no sense without the exception)

In this case the condition is that it happens after a return keyword.

_ is used as a placeholder in for loops and voids the assignment. Yes you can use _ as a variable, but, that’s not exactly a great variable name in any case since it has cases where it can’t be used at all.

And again, I said in my post that assignments becoming expressions is problematic but in this case it would not be treated as an expression, it would be treated as unique syntax under the specific circumstance which avoids that.

My examples are not a proposal, they are just there represent why being able to do this would be useful and showing that this can be done while still avoiding these common syntax issues with these sorts of requests. My examples were something I came up with in 5 minutes and all I did was use the first option I came up with which satisfied syntax conditions.

If assignment replaced the possibility of an expression list, it would be very complicated to parse.

return a()[b()],c()[d()],e()[f()]

and

return a()[b()],c()[d()],e()[f()] = g,h,i

To figure out if return was an assignment return or a normal return, the parser would have to look arbitrarily far ahead to find the disambiguating token (an equal sign, a semicolon, an end, an expression which couldn’t be used in an assignment, or the end of the program).

It would be weird if this specific assignment treated _ specially, when all other assignments would assign to _.
Where can _ not be used?

local value = current or default
current = value
2 Likes

If default is false this fails.
current ~= nil or default would work, but, still, the point is to do this performantly. This constantly is doing an assignment every time you access it. It’ll invoke metamethods every time and this is not something you usually want. For instance properties that would invoke Changed events every time for example. This is something I usually want to avoid because it can add up to a pretty big percentage of time even for 100 executions and a lot of my use cases are where I’m doing something hundreds of thousands of times, like in terrain generation.

This isn’t really a solution to the use cases I’m talking about.

@Halalaluyafail3 Let’s stop arguing about how I came up with an example because again its the first thing I came up with after 5 minutes, it was simply meant to be an example of how a feature like this would be useful, and, when I came up with those my intentions weren’t to come up with a realistic syntax.