Is there a better way to "fix" recursively requiring modules w/o requiring the module each time it is needed & without any external dependencies?

So I’m working on managing a placement system, under certain scenarios people should not be able to start and end an object’s placement. In order to do this, I need to bar the placement using a variable or something similar. The problem is that with the way my UI objects and build mode controller are set up, it causes the modules to be required recursively.

So, as some background, I have the main build mode controller, “BuildModeHandler” and my UI object controllers as the children of the build mode controller (GridSlider) so it’s laid out like the following:
image

The problem is that inside of “BuildModeHandler” there are constant variables that point to GridSlider (BuildModeHandler.GridSlider = require(script.GridSlider)). Now, I need to update a variable inside of BuildModeHandler from GridSlider when the mouse is inside of GridSlider, thus creating a cyclic dependency.

To fix this, I can just make a third module called “status”, however this is a bandaid method and is not really ideal. In my case, the best case scenario would be to have no external dependency, eg. no value objects, no attributes, no third module, just the two modules. Is this possible?

Relevant part of the script for reference, GridSlider is the grid slider object, an object that represents multiple UI objects but visually looks like a slider element:

module.GridSlider = require(script:WaitForChild('GridSlider'))
module.GridSlider:UpdateValue(2.5)
module.GridSlider.ValueChanged:Connect(function(value)
	module:AdjustGridSize(value)
end)

Inside of GridSlider, this will cause an error in the output:

local buildModeHandler = require(script.Parent)
gridSize.MouseEnter:Connect(function()
	buildModeHandler.BarPlacement = true
end)

Now, the solution would appear to be this:

gridSize.MouseEnter:Connect(function()
	require(script.Parent).BarPlacement = true
end)

Sure, this works, but is not ideal for a few reasons:
1- Every time the mouse enters the UI object, a call to require the module is made.
2- Won’t work if for some reason the module’s ancestry is changed.
3- There is still a warning:

Any other ideas?

3 Likes

You could use a bindable function to request for the table of the requiring module.

I tested this and it works for me

Module1

local module1 = {a = 1}

function script.RequestModule.OnInvoke() -- Handle incoming request before requiring the modules
	return module1
end

module1.Other = require(script.Module2)

print(module1.Other)

return module1

Module2

local module2 = {b = 2}
module2.Other = script.Parent.RequestModule:Invoke()

print(module2.Other)

return module2

Hierarchy
image

Output
image

But as you can see, I required Module1 using the command bar so I’m not sure if it will actually work in-game.

Edit: I just realised, this should have made a cyclic table but it didn’t. This probably proves that the table module being requested is a copy of the original table.

I also delayed the print to check if the table was actually the same and apparently not.

Module2

local module2 = {b = 2}
module2.Other = script.Parent.RequestModule:Invoke()

task.delay(1,function()
	print(module2.Other)
end)

return module2

Output:
image

module2.Other should have printed what Module1 printed. and the cyclic table module.Other.Other.Other… isn’t there
image

1 Like

If you have two or more ModuleScripts that require each other’s content would it not be better to simply merge the ModuleScripts?

--MODULE A

local Script = script
local ReplicatedStorage = Script.Parent

local ModuleA = {}

function ModuleA.PrintA()
	print("A")
end

return ModuleA
--MODULE B

local Script = script
local ReplicatedStorage = Script.Parent

local ModuleB = {}

function ModuleB.PrintB()
	print("B")
end

return ModuleB
--MODULE C

local Script = script
local ReplicatedStorage = Script.Parent

local ModuleC = {}

ModuleC.ModuleA = require(ReplicatedStorage.ModuleA)
ModuleC.ModuleB = require(ReplicatedStorage.ModuleB)

return ModuleC
--SERVER

local Game = game
local ReplicatedStorage = Game:GetService("ReplicatedStorage")
local ModuleC = require(ReplicatedStorage.ModuleC)

ModuleC.ModuleA.PrintA() --A
ModuleC.ModuleB.PrintB() --B

How feasible would a ModuleScript Venn diagram be? In other words, a ModuleScript that requires multiple ModuleScripts.

There are a few ways to resolve circular dependencies (one of which you mentioned with the use of a third module on which both can depend). If you do not want to use that, you could do dependency injection by having the BuildModeHandler pass a reference to itself to the GridSlider. If I were in this situation though, I would think that the GridSlider does not necessarily need to know about the BuildModeHandler. Instead, the GridSlider can generate events for which interested observers (in this case the BuildModeHandler) can subscribe to and implement some logic for. So instead of the GridSlider directly modifying the BarPlacement property, it would inform its observers that a MouseEnter event was generated, and the BuildModeHandler would update the property itself. There are other solutions as well, but these are some to start.

2 Likes

Ideally I would like to avoid any external dependencies (eg. a third instance).

This was another idea I was considering, however it comes back to organization. The grid slider won’t be the only UI object, there will be a variety of objects (probably like 10 at least), at 50 lines average per object (probably more), that’s 500 more lines for BuildModeHandler which once again is not ideal.

This is a good idea actually, was thinking of doing this but using metatables (so using the __index metamethod to point to each of the modules). I actually do this in my UIObject module:

local module = {}

module._funcs = {}
for i,v in ipairs(script:GetChildren()) do
	module._funcs[v.Name] = require(v)
end

module.__index = function(self, index)
	if module._funcs[self.Type][index] then
		return module._funcs[self.Type][index]
	else
		if type(self._extends[index]) == 'function' then
			return function(this, ...)
				return self._extends[index](self._extends, ...)
			end
		else
			return self._extends[index](self._extends[index])
		end
	end
end

The issue with this, however, is that it doesn’t necessarily fix the cyclic dependency issue. Since one module would be requiring all of the sub-modules, to access another module I would still have to go through this topmost module, still creating a cyclic dependency.

This seems like the best option I think just when comparing it with the other option, mainly because I already wrote the events and whatnot inside of GridSlider, implementing an event listener for the UI objects inside of BuildModeHandler is just not really what I’m going for I guess. Thanks!

1 Like