Help with using setfenv to make predefined globals

Hello!

Basically I made a few iterator functions- ripairs, apairs, and rapairs (reverse ipairs, alphabetic (actually lexical) pairs, reverse alphabetic pairs). Basically I’m looking for a seamless implementation which generally is going well except when I am calling a function from another function, the “global” doesn’t exist.

Here’s the module:

local module = {
	ripairs = function(tbl: {any})
		local index = #tbl
		return function()
			local initIndex = index
			local initValue = tbl[index]
			if initValue then
				index -= 1
				return initIndex, initValue
			end
		end
	end;
	apairs = function(dict: {[any]: any}, sortFunction: ((any, any) -> boolean)?)
		local sortedIndexes = {}
		for i,v in pairs(dict) do
			table.insert(sortedIndexes, i)
		end
		table.sort(sortedIndexes, sortFunction)
		local index = 1
		return function()
			local initIndex = sortedIndexes[index]
			local initValue = dict[initIndex]
			if initIndex then
				index += 1
				return initIndex, initValue
			end
		end
	end;
	rapairs = function(dict: {[any]: any}, sortFunction: ((any, any) -> boolean)?)
		local sortedIndexes = {}
		for i,v in pairs(dict) do
			table.insert(sortedIndexes, i)
		end
		table.sort(sortedIndexes, sortFunction)
		local index = #sortedIndexes
		return function()
			local initIndex = sortedIndexes[index]
			local initValue = dict[initIndex]
			if initIndex then
				index -= 1
				return initIndex, initValue
			end
		end
	end;
	pairs = pairs;
	ipairs = ipairs;
}

return setmetatable(module, {__call = function() 	-- note the __call metamethod
	local fenv = getfenv(2)
	for i,v in pairs(module) do
		fenv[i] = v
	end
end})

Then to “deploy” it, require the module and call the table, invoking the __call metamethod:

require(modules:WaitForChild('Utility'))()

However, when I call a function within a function, for example here:

local function effect()
	for i,v in rpairs(order) do -- attempt to call a nil value
        -- not important
		local container = script.Parent:FindFirstChild(v .. 'Container')
		if container then
			local mousePositionOnButton = Vector2.new(mouse.X - container.AbsolutePosition.X, mouse.Y - container.AbsolutePosition.Y)
			
			local blackCircle = Instance.new('Frame')
			local uiAspectRatioConstraint = Instance.new('UIAspectRatioConstraint')
			uiAspectRatioConstraint.Parent = blackCircle
			uiAspectRatioConstraint.AspectRatio = 1
			blackCircle.BackgroundColor3 = Color3.new()
			blackCircle.AnchorPoint = Vector2.new(0.5,0.5)
			blackCircle.Size = UDim2.new()
			
			blackCircle.Position = UDim2.fromOffset(mousePositionOnButton.X, mousePositionOnButton.Y)
			blackCircle.Parent = container:FindFirstChild('InitEffect')
			
			tweenService:Create(blackCircle, TweenInfo.new(1), {Size = UDim2.new(0,50,0,50)}):Play()
		end
	end
end
------------------------
local function out()
	for i,v in ripairs(order) do --> ok
		local container = script.Parent:FindFirstChild(v .. 'Container') :: Frame
		if container then
			coroutine.wrap(function()
                -- not important
				local initEffect = container:WaitForChild('InitEffect') :: Frame
				local initAbsoluteSize = container.AbsoluteSize.X
				initEffect.Position = UDim2.new(0,0,0,0)
				initEffect.AnchorPoint = Vector2.new(0,0)
				tweenService:Create(initEffect, tweenInfo, {Size = UDim2.new(1,0,1,0)}):Play()
---------------------------------
				effect() -- note this is where I'm calling the second function
---------------------------------
                -- also not important
				task.wait(0.25)
				for i,v in pairs(container:GetChildren()) do
					if v:IsA('GuiObject') and v ~= initEffect then
						v.Visible = false
					end
				end
				initEffect.AnchorPoint = Vector2.new(1,0)
				initEffect.Position = UDim2.new(1,0,0,0)
				tweenService:Create(initEffect, tweenInfo, {Size = UDim2.new(0,0,1,0)}):Play()
			end)()
			task.wait(0.1)
		end
	end
end

Is there any way to get around the error without having to manually define the functions, and without having to index the functions through the module?

1 Like

Bump, still unsure on how to proceed.

Why do you want to use setfenv in 2022 instead of just requiring the module and making the functions you’ve defined in it locals at the top of your script where it’d be accessible to your other scopes? :confounded:

2 Likes

because i am lazy and need the easy way out

If anything the fact that you have a help thread doesn’t really indicate laziness. I’m not sure why you’d want to rely on bad practice to save a couple of seconds writing but power to you I guess if you’re okay with it and know the downsides of modifying the environment.

Personally, I don’t use or ever want to use get/setfenv and don’t deeply understand them but consider if your functions are actually getting the environment you’re setting and it’s not just a shallow set. You were able to inject your custom iterators as globals to the out function but effect is called inside a coroutine. You should check in this case if the globals exist:

  • In the effect function when called independently of out (special case out your test with a parameter to print the environment and then prematurely return to end the function

  • In the coroutine created by out at the top before you call effect

I also don’t know where the utility gets required (at the top of the script as an upvalue?) or what the second script is (a ModuleScript? is out being returned?). Last thing is that, well, have you experimented with stack level 0? That returns the script’s environment, not a caller function’s. Stack level 2 may not be sufficient enough to catch the effect call.

3 Likes

Yeah, I think I made a post awhile ago asking for it and you replied talking about the optimizations of modifying the environment but that was very long ago, like a year or so. Probably more but that isn’t really relevant.

Utility script is required at the top of the script, yeah. At the end of the script that’s where the environment is set. Second script is just a script that controls the UI effects. And no, I haven’t tried with level 0, I thought lower levels meant it wouldn’t like go to another level, I thought it was like

function a()
 -- this is level 3
end
function b()
 -- this is level 2
 a()
end
function c()
 -- this is level 1
 b()
end
c()

But maybe not, I might have the wrong idea of what a level is, probably wouldn’t hurt to look into that.

Anyway, about your debug things, I’ve done a few things:
I wrapped the code that calls out() in a function (_in) called _in(), printed ripairs in _in() which printed nil, the calls to the other functions also printed nil

I also messed around with the level you talked about while keeping the _in function, basically keep in mind that _in calls out, I also printed ripairs inside each of these functions

Level 0:
not in function call - function
_in - nil
out - function
coroutine - nil
in mousebutton1click - function

Level 1:
all nil

Level 2:
not in function call - function
_in - function
out - function
in coroutine - nil
in mousebutton1click - function

Like @colbert2677 said why not just use a something like vscode snippets to do it?

Or use a plugin to edit the script source to automate it for you.

I believe this will be much faster, easier, and optimal.

Otherwise, I believe testEZ does something similar with injecting functions such as describe, it, and expect but not sure about these context levels for production use code.

I looked into the snippet thing and that seems pretty cool, probably the best option for not modifying the fenv, I’ll consider that but really I don’t think it’s much different than writing out the getservice, getting the correct children from replicated storage etc, although I suppose writing it out once and never again would still be saving my time, but it isn’t really something I can learn from like this is. It’s still a good idea though and that’s probably the route I’ll go if modifying the environment doesn’t work.

I’m not sure that testEZ is what I’m looking for.

Still, I believe it’s pretty odd, I have done some testing and it seems to work and I’m able to print out the function with the module script you gave at the start.

Test code for copy and paste
require(script.ModuleScript)()

local function a()
	print(ripairs)
end

local function b()
	a()
	
end

local function c()
	b()
end

c()

Not sure whats happening, is this the wrong scenario? something else happening?

My first thought for experimenting was to make every function a global variable so you could get it with getfenv(). But yeah that’s too much work sorry.

It doesn’t work if you wrap in a coroutine tho. If you do coroutine.wrap(b)(), I’m pretty sure that would cause it to be nil.

Nope?

BTW is it, rpairs, or ripairs? or rapairs?

I dont’s see rpairs in the module script you gave out, is this what is causing the errors?

1 Like

Really? What environment level are you using?

ripairs

I didn’t realize I made that mistake omg. I feel so stupid lmao thanks for pointing that out. That literally fixed it.

1 Like

NP, thanks for posting the module script really helped with the test debugging.

Hence I also recommend just opening a empty baseplate and just going wild.

1 Like

Yeah, it was really frustrating me though, I can’t believe I overlooked that. But I suppose having values that are nil to the interpreter doesn’t help because you just assume it’s always going to say it’s nil even when it shouldn’t be.

1 Like

Yep to debug I would go further and use getfenv as a debugging tool as well I believe this is new to me and you.

print(getfenv())

all the global functions will be available like right there.

image

1 Like