Allow Passing Arguments through Connections

Many libraries such as task and pcall allow you to pass a function and its arguments through, which is preferred over creating a closure every time, which may potentially never cache.

local function SetProperties(Part: BasePart)
	Part.Color = Color3.fromRGB(255, 0, 0)
	Part.Position = Vector3.new(5, 10, 15)
	Part.Transparency = 0.5
end

task.spawn(SetProperties, workspace.Part)

-- VS
task.spawn(function()
	SetProperties(workspace.Part)
end)

However, we do not have this functionality with RBXScriptSignal connections, meaning we are forced to create uncached closures potentially every frame.

-- Class method which must be localized
function self:OnPreSimulation(DeltaTime: number)
end

function self:Start()
	RunService.PreSimulation:Connect(function(DeltaTime: number)
		self:OnPreSimulation(DeltaTime)
	end)
end

The Proposal

Make RBXScriptSignal/Connect and its siblings take an optional vararg which is sent to the beginning of the callback’s parameters when ran.

function self:Start()
	-- Same as self.OnPreSimulation(self, DeltaTime)
	RunService.PreSimulation:Connect(self.OnPreSimulation, self)
end

This would also improve the OnPlayerAdded-OnCharacterAdded idiom by allowing us to pass the player argument over, so we don’t have to make a lambda for every join.

local function OnCharacterAdded(Player: Player, Character: Model)
	local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
	
	if Player.UserId == 123 and Humanoid then
		Humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
	end
end

local function OnPlayerAdded(Player: Player)
	if Player.Character then
		OnCharacterAdded(Player.Character)
	end
	
	Player.CharacterAdded:Connect(OnCharacterAdded, Player)
	
	-- VS
	Player.CharacterAdded:Connect(function(Character: Model)
		OnCharacterAdded(Player, Character)
	end)
end

Players.PlayerAdded:Connect(OnPlayerAdded)
3 Likes

which is preferred over creating a closure every time

If you’re hoping to avoid the overhead, this would not do that. In order to hold onto the arguments you want to later pass, the connection would need to store additional internal Lua state in order to hold those arguments, state which would probably clock in at being more expensive than the additional closure you would have been using in the first place.

task.spawn can do this efficiently since a new Lua thread, which can hold the arguments, is immediately being created either way by that function, but connect would actually have to create additional state that otherwise wouldn’t exist to store the args.

TL;DR: It’s hard for the Connection infrastructure to actually do this more efficiently than the closure is already doing it, since closures are a core feature of Lua and already highly optimized.

7 Likes

I know this is just a summary of the inner workings, but don’t connections already create their own thread that is reused every invocation? Why would storing the arguments in that thread be any worse?

4 Likes

Because you need two threads, a temp thread to store the arguments on until there is an invocation and the thread running the invocation which has those arguments copied to it. A single thread storing the arguments isn’t enough because there may be multiple simultaneous invocations of the signal in flight at once.

There may be a way to juggle things just right by reusing a single thread pre-loaded with the args when there aren’t simultaneous calls and lazily creating a second storage thread the first time there are. However that still probably wouldn’t be appreciably better than the closure perf while having much higher complexity.

6 Likes

You can use Lemon Signal, it has the feature you want + more


@stravant thank you for the detailed explanation, I always appreciate the extra information especially coming from a reliable source

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.