Possible usage for top-function Vararg?

So, this is somewhat strange, but I recently noticed that Roblox’s Lua still works as normal Lua would, and compiles script protos with a IS_VARARG (that means you can use the ...) tag to them.
I then began to wonder, what could this be used for? And then it hit me.
ModuleScripts.

The ability to require(ScriptOrId, Args...). This could help out modules when you don’t want to return a function, and want the code to run once anyways to return something (and, due to cacheing, could be used as a way to do great things like a module vararg cache).

Here are my findings:
image

image

This just seems like an easy-to-implement feature ready to happen, and would help out in thing such as shared modules, which need to adapt to the machine they run on, or just simply helping out on making the scripting of modules a lot faster due to a direct access to the variables being passed, and without need naming them.

Alternatively, it could always be used to pass debug data about the script or any other miscellaneous uses that could help developers out.

Side note: I put this under Roblox Discussion because I am not sure if it belongs in a feature request (as it’s a rather obscure topic anyways).

Side side note: Moved to Client Features in hope it’ll get taken into consideration by the Studio team.

9 Likes

Make it a feature request for studio

My god I was waiting for this

:slight_smile:

“This is not a good idea because the return value of a ModuleScript is cached when its first required and so the ModuleScript itself will never run again. If some script A required the ModuleScript and passed some arguments to require and another script B required the ModuleScript and passed a different set (or no) arguments, there will be a problem. The order scripts start running in isn’t technically defined so you will have undeterministic behavior. The state of the ModuleScript will depend on whichever script happened to run first.

A solution could be to yield (similar to how people used to wait until some _G entry existed) but that’s not a general nor robust solution. Something like this only makes sense to a function like dofile where the script would be run every time.

The reason the top-level function of scripts (and in extension, ModuleScripts) are variadic is for command-line arguments. It just semantically doesn’t make sense to have a the state of a ModuleScript depend on what’s requiring it.”

11 Likes

The require function should not be used as a way to perform operations. It should only be used to retrieve a library or other data type, which is then used to perform operations.

8 Likes

No, this is a client feature, as it can be used by in-game scripts as well. Studio Features is for features exclusive to Studio (ribbonbar, plugins, etc)

1 Like

Woops, will move then.

2 Likes

Ideally though, you wouldn’t have these sort of cases happening, unless you’re doing something such as using a ModuleScript to share a data set or the like, and the more specific use case I’m referring to is the following:

local Debounce = false;

return function(...)
    if Debounce then
        return customedData;
    else
        Debounce = true;
    end;

    -- Stuff to do with ..., and an ugly debounce to get customed data

    return customedData;
end;

versus

-- Do some stuff with ..., and no debounce

return customedData;

These are used in the majority by admin scripts, or for using settings moduldes/modules that need settings, or even for hierarchies of modules which each depend on the lower ones (which is used a lot, and you end up having 1 module require all its children modules and interfacing with them, which nullifies the problem).

Additionally, modules are mostly used to order code, as opposed to the desired (or original intent?) of sharing functions across more than 1 script more often than not from what I’ve seen.
Another use case, regarding shared modules, would be using a single module for the client and server, which would take different inputs depending on which machine it runs in, and returns the appropriate result.
For example, that of a “Networking” module, which is passed some info on the server about remote names, parents, and maybe even server version or other misc data to make sure it’s all working together; the client is then provided locally with some different information that the module would then use, and respond differently.

There are probably hundreds more niche cases, if not for the more obvious ones, which likely would see a wide usage (some I previously mentioned).

1 Like

This prevents require from accepting any other parameters in the future.

I’m not a fan of this way of doing things – it seems too prone to bugs and errors. It’s not up to me or Roblox to enforce coding style, so I don’t think that’s a good argument against this feature request. With careful usage and argument checking, it can be used in ways that cause few bugs and are easy to debug.


If anyone's looking for examples of how to do this now, I wrote some.

Here’s a simple example that does all the run-once debounce checking in one module:

local result
local function run(...)
	-- your run-once code goes here
	return -- whatever you return is returned every time after the first run
end

return function(...)
	if not result then
		result = {run(...)}
	end
	return unpack(result)
end

Here’s a multi-module example that uses multiple modules to make the run-once modules look a bit cleaner:

--- ReplicatedStorage.runOnce
return function(run)
	local result
	return function(...)
		if not result then
			result = {run(...)}
		end
		return unpack(result)
	end
end
return require(game.ReplicatedStorage.runOnce)(function(...)
	-- your run-once code goes here
	return -- whatever you return is returned every time after the first run
end)

With either of these, you get the following syntax to use your run-once modules:

local result = require(yourRunOnceModule)(yourArgs)

That’s the sort of syntax I’m trying to evade tho; the extra function call and having to pass args to it in that way.
The idea is to make use of vararg to omit using extra code and checks in every module to make sure it runs once, while also having a means of passing information in case the module needs to be initialized with it.

1 Like

You skipped the undeterministic behavior point. Which script’s arguments will be used, A or B?

A:

require(game.ReplicatedStorage.ChildrenRenamer, game.Workspace.SomeModel)

B:

require(game.ReplicatedStorage.ChildrenRenamer, game.Workspace.OtherModel)

ChildrenRenamer:

local model = ...
for i,v in pairs(model:GetChildren()) do
  v.Name = i
end
return model

It doesn’t matter whether this exact code would be useful, but it still raises the question on which script would get prioritized.

2 Likes

It’d be acting as a normal require, so it’d halt the other threads until it gets a return with whatever required it first.
As @Sharksie mentioned, ModuleScripts are not supposed to be doing operations, but operations are not the point of this post. The point of this post is to prepare the ModuleScript to do operations with a specified set of starting values, or to provide extra information to it that you wouldn’t want contained in it to begin with.

1 Like

I’m not convinced that this would make anyone’s life easier.

It’s unclear how requiring a module multiple times with different args would work. There’s two possibilities…

  1. Only the first require sets the varargs
  2. Every single require overwrites the varargs

Both produce diffent outcomes based on the dependency resolution order, which is a big red flag…

4 Likes

@Rerumu sigh

Could replicate in a way that doesn’t require you to wrap your code in a function:

wait() -- not required, just used for the script at bottom of this post

if not xpcall(function() getfenv(4) end,gcinfo) then
	return setfenv(1,getfenv())(1,2,3)
end

print(...) --> 1 2 3

return tick()

the weird xpcall thing is so 1,2,3 can be false or nil and not generate an infinite loop

Doing print(require()) twice outputs 1 2 3 \n 1512471965.4277 \n 1512471965.4277.

Bonus: If you use return setfenv(1,getfenv()) (no call) instead, this is possible:

local req = require(module)
req(1) --> prints inside module prints 1 and 1512472330.5101
req(2) --> prints inside module prints 2 and 1512472330.5439

or just do regular stuff

2 Likes

So you want to configure your module. It’s still the same problem.

A:

require(Module, ConfigA)

B:

require(Module, ConfigB)

Now your module has a non-deterministic configuration.

The only case I can think of where is would make any sense is configuring for multiple security contexts. Since a module is required once per context, it may be useful to configure the module, such as to avoid accessing a secured API in a less privileged context.

Of course, running a module in multiple contexts is relatively rare. And there are already ways to determine the context automatically, with no manual configuration required (i.e. just pcall it). And the lack of determinism is still a problem.

Regarding the undeterministic results, I’ve already addressed this as a problem, but it being a module should just require and pass the vararg for whatever required it first, as opposed to doing so every time.

This issue is comparable to having a script that depends on being parented to something specific to work, but is parented to something else before it runs; this would be a “change” in the conditions it runs under, and would more likely be blamed on a developer practice instead.

1 Like

But what do you define as “first” in case of two scripts stored at the same location and requiring the module in very first line?

Both scripts are identical, excep for the require arguments. Please take your time and let this situation sink in your head.

1 Like

I don’t see the issue?
There isn’t one.
Do the same with modules as is right now, and call error() in it; you’ll see the error outputs.
Modules as are right now yield all other requirers until the first one’s thread returns, at which point the module will too.

1 Like

Yes, but right now there is no difference which script ends up requiring it the first. It will matter when there will be a way to pass extra arguments to the require function.

I’m sorry, but just because you’ve discovered a pretty cool Lua behavior, doesn’t mean it would make sense to support it.

When implementing new features, you have to consider all the gains and all the disatvantages that this change would bring. And while weighting the risks, also remember that once they release the feature, there is pretty much no way to undo it, because that will end up breaking code that is relying on the new feature.

If the argument against is that you should just not use it because it could have a possibly ambiguous case, then we shouldn’t have support for things such as:

Reparenting scripts
Destroying running scripts
or resuming threads with coroutine.resume() which were yielded using wait()

It’s an issue when not used properly, such as for actual operations, as opposed to preparing a module for such operations.
A fine example could be using vararg to pass a salt to a hashing module and return a function that uses it vs passing a salt and string to hash.

1 Like

Not sure what you’re getting at.
None of those rely on dependency resolution order.