Add variable hoisting to Lua

I’m reprogramming all the swords in SFOTH to minimize code replication.

For neatness, I’d like to subscribe to all the events my script requires at the top of my code file.

However, because Lua has no variable hoisting, I can’t reference a function that is defined later in the file. So half of my connects() are at the top of my file and the other half are at the bottom.

Just one more thing that makes it harder to write clean code in Lua.

image

Who wants to write a 2 pass Lua compiler?

Doing a forward function definition makes the code worse because it’s not as simple as just defining onEquipped as a symbol that will exist later. I have to define it as a dummy function and then later in the code swap it, unless I’m missing something.

14 Likes

You are missing something, it’s perfectly valid to forwards declare a function in Lua:

local myForwardsDeclaredFunction

function testCaller()
	myForwardsDeclaredFunction()
end

function myForwardsDeclaredFunction()
	print("Called!")
end

testCaller() --> Called!

This is an unfortunately little known feature of the Lua syntax.

7 Likes

I’m missing something subtle, because that is the first thing I tried.

This doesn’t run because “Passed value is not a function”.

image

Maybe it is legal Lua syntax but the Roblox API does some type checking? How do I forward declare a function that connect() will accept?

3 Likes

Oh, I see what you’re trying to do. There’s unfortunately no way that this could work in Lua.

The connect function needs to take an actual closure (function object), and until the script has run to the point where your function gets defined, there is no closure.

The best you can do is create a wrapper which you pass to Connect:

local onUnequipped

Tool.Unequipped:Connect(function() onUnequipped() end)

function onUnequipped()

end

Variable hoisting (while possibly a nice to have for other reasons) will not be able to fix that problem.

4 Likes

That’s because line 6 is referring to a function which doesn’t exist yet. But in @tnavarts example, its being used in the testCaller function. It’s already been declared by the time it’s called. It’s using the global variable myForwardsDeclaredFunction. You can do this with a declared local variable too (here, myFunc):

local myFunc

function callMe()
     myFunc()
end -- After this point, you can now refer to callMe as a function instead of nil

funciton myFunc()
    print("Yep, it works")
end

callMe()

Edit: Didn’t notice his example also specified that it was local. You don’t actually need to do this - it’s just a matter of the current scope having that variable set.

1 Like

I might have a possible solution if you’re willing to try it. Since I’m not entirely sure how your codebase is setup, you could try an OOP solution using Janitors/Maids and fake ValueObjects. I’ve managed to bypass the problems you’re having entirely by doing this. The attached file does demonstrate it.

It avoids the problem you’ve been having by using the fake ValueObject class and the Changed property it has to update the appropriate function whenever it’s changed. It then removes the connection using the Janitor object to prevent memory leaks. This allows the order of it to never even matter in the first place, and even allows changing whenever you feel like it.

BaseSword.rbxl (34.8 KB)

A simple alternate solution you could try is to organise some of your functions using ModuleScripts.

From there, you can simply require the module whenever you need to use them. Simple and clean :slightly_smiling_face:

2 Likes

We already have a multi-pass Lua compiler, the issue is that the execution semantics is top-to-bottom including function declarations.

While we could in theory model JavaScript semantics here, I don’t know that this is a good idea. If anything it invites issues with calling functions that use variables that aren’t declared yet, because functions and variables play by different rules. In contrast, Lua semantics is clean and easy to understand.

This could get easier if at some point we add first-class OOP support, perhaps.

8 Likes

I just want to write nice code in something that is sort of like a modern language.

4 Likes

(This is a bit shoehorned in, but I want to comment real quick on the technique you’re using to make a Sword module. This can solve the problem you’re having here, and also make your sword more robust.)

You’re on the right track, but there are some things you could be doing differently to make it easier on yourself. I’m about to dump a lot of info which I explained to @loleris the other day.

If you’re willing to hear me out, I think this might be the long-term solution you’re looking for :slight_smile:


Lua has syntax sugar that allows you to create pseudo-classes from a table through the usage of:

  • Metatables
  • The : operator
  • The self variable.

Here’s a functional example class that can be constructed:

local Class = {}
Class.__index = Class

function Class.new(a, b, c)
    local inst = 
    {
        A = a;
        B = b;
        C = c;
    }
    
    return setmetatable(inst, Class)
end

function Class:WhatIsA()
    print("My value for A is", self.A)
end

function Class:WhatIsB()
    print("My value for B is", self.B)
end

function Class:WhatIsC()
    print("My value for C is", self.C)
end

function Class:LogAll()
    self:WhatIsA()
    self:WhatIsB()
    self:WhatIsC()
end

function Class:GetValueSum()
	return self.A + self.B + self.C
end

return Class

Using this code pattern, you can create modules that can have their functions overloaded by defining those functions on a new instance of that module.

In theory you could create a Sword ModuleScript that allows swords to hook up their specific functionality. Here’s some potential pseudo-code without an actual implementation for the Sword module:

local IceDagger = Sword.new
{
	Tool = script.Parent;
	SlashDamage = 10;
	LungeDamage = 35;
}

function IceDagger:OnEquipped()
	-- TODO
end

function IceDagger:OnUnequipped()
	-- TODO
end

function IceDagger:OnActivated()
	local now = tick()
	
	if (now - self.LastClick) > 0.25 then
		self:Lunge()
	else
		self:Slash()
	end
end

function IceDagger:OnVictimHit(humanoid)
	-- Logic for freezing the hit humanoid.
end

Through the magic of __index, calling self:OnActivated() in a function of the Sword module would “downcast” to the functions declared in the IceDagger script.

You could setup event handlers and other general logic shared by all of the swords in the Sword module, while the overload functions defined in IceDagger would handle the functionality specific to the Ice Dagger.

A great talk was done about this at RDC 2019.
Skip to 19:49 if you’re interested in the bit relevant to what I’m talking about here:

Let me know if you have questions!
Sorry in advance for the verbose posts lol.

11 Likes

I’d recommend avoiding global state like that anyway. Try structuring your code like this, with anonymous functions.

local function onClick(handle, toolState)
	if toolState.lunging then
		return
	end

	toolState.lunging = true
	delay(1, function()
		toolState.lunging = false
	end)
end

local function onTouch(tool, toolState)
	if toolState.lunging then
		print("On touch, maybe damage lunge")
	else
		print("On touch, maybe damage normal")
	end
end

local function onEquip(tool, toolState, mouse)
	print("on equip")
	local handle = tool:WaitForChild("Handle")

	tool.Handle.Touched:Connect(function(part)
		onTouch(part, toolState)
	end)

	mouse.MouseButton1Down:Connect(function()
		onClick(handle, toolState)
	end)
end

local function onUnequip(tool, toolState)
	print("on unequip")
end

local function handleTool(tool, toolState)
	tool.Equipped:Connect(function(mouse)
		onEquip(tool, toolState, mouse)
	end)

	tool.Unequipped:Connect(function()
		onUnequip(tool, toolState)
	end)
end

-- Why is this nice?
-- Well, first of all, we don't have to worry about the global variable 'tool'
-- and where it comes from.
-- Also, it fixes the above issue with declaration order.
-- And finally, each function is self contained.
handleTool(script.Parent, {
	damage = 10;
	slashDamage = 25;
})

Where you pass things around using closures, and a table for tool state. :smiley:

You really don’t want all those global variables anyway. Much easier to reason about this way.

I didn’t test this code, but I’m guessing it’s close to what you want.

Note this probably leaks connections, but AFAIK, mouse is destroyed on unequip, and if your sword is destroyed on removal, you’ll be good (given an individual script).

Edit: Actually it definitely leaks connections with the touched event. Just track the touched event conn, and pass it into the unequipped handler to be disconnected. **

5 Likes

You’re right about global state. I didn’t realize it was global.

Is there an easy/elegant/good way to enforce this contract?

I want to define a class or struct or interface with properties in a way where I can’t accidentally forget to set damage = 10 (or worse, typoing the variable name/capitalization and not getting a compile error) and have a bug happen at runtime when I create a new sword. For an object with a lot of properties, this inevitably becomes a copy pasta.

The best I’ve come up with is I create a table at the beginning of my script that has default values for each property defined and in my constructor I clone that table. It works for value types but not reference types and it still feels like a crappy bolt-on.

At least I can look at this code and tell what properties this object has though.

2 Likes

Make a module script like this:

-- Module script:
local SwordConfigUtils = {}

function SwordConfigUtils.create(config)
	-- This may seem redundent at first, but it actually isn't. This defines the structure of your data
	-- struture, and also defines what can/can't be nil. Also means you can have multiple
	-- constructors. For example, you could have SwordConfigUtils.makeBigSword() which passes in different types.
	-- For this, I use swordType = "hi"; at the top.
	return {
		swordDamage = assert(config.swordDamage);
		slashDamage = assert(config.slashDamage);
	}
end

return SwordConfigUtils

And then try this.

-- Require this in a more safe way if you want... I do my requires at the top of the script.
local SwordConfigUtils = require(game.ServerStorage.SwordConfigUtils)

...

handleTool(script.Parent, SwordConfigUtils.create({
	damage = 10;
	slashDamage = 25;
}))
1 Like

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