1

This is a dummy variant of a setup I'm running. The Launcher function does not seem to see the GetLevel. At least the first print is seen but the second isn't. So the function never starts. Is that because the code comes after the place it is launched it in the code, and at the point Launcher executes it doesn't exist yet? When I remove the local tag it works. So global functions can be "seen" from anywhere in the script, but local ones only from later in the code? I'm trying to find out which functions I can make local, as they are all currently global. Is there even a benefit to this - is there a difference between a function that's local to the entire script and one that's not local?

local UseLevel = true
local Limit = 0

local function Launcher()
  print("trying to launch GetLevel now")
  GetLevel()
end

local function GetLevel()
  print("GetLevel was launched")
  if UseLevel then
    Limit = 4
  else
    Limit = 5
  end
end
JohnBig
  • 97
  • 7

3 Answers3

3

Is there even a benefit to this - is there a difference between a function that's local to the entire script and one that's not local?

The benefit of using locals is twofold:

First, locals are usually faster, as they simply use registers in Lua's virtual machine whereas globals are entries in the global table (which is effectively a hash map). Calling a global function requires first indexing the hash map.

Second, locals help improve code quality, as they are only visible in their local scope (say, a file, or even within an if-branch, a loop or the like). Putting everything in the global scope is known as "global pollution" and frowned upon. It also slows down global access as it bloats the global table. Note that Lua allows changing the environment using _ENV (or setfenv in older versions), so you can sometimes save yourself both the local keyword and forward declarations by doing so without polluting the global environment. Usually you shouldn't change environments though.

Globals are often used to expose interfaces as pointed out by Nicol Bolas. You can alternatively use return my_api_table at the end of your files for this; the API table can then be "imported" using local my_api_table = require(relative_path).

Is that because the code comes after the place it is launched it in the code, and at the point Launcher executes it doesn't exist yet?

Yes. You can fix this using a "forward local declaration" of GetLevel:

local UseLevel = true
local Limit = 0
local GetLevel -- "forward declaration" for Launcher

local function Launcher()
  print("trying to launch GetLevel now")
  GetLevel()
end

-- This is the same as GetLevel = function() ... end and thus does not set a global variable
-- but instead assigns to the previously declared local variable GetLevel
function GetLevel()
  print("GetLevel was launched")
  if UseLevel then
    Limit = 4
  else
    Limit = 5
  end
end

Simply putting the Launcher function below GetLevel also works:

local UseLevel = true
local Limit = 0

local function GetLevel()
  print("GetLevel was launched")
  if UseLevel then
    Limit = 4
  else
    Limit = 5
  end
end

local function Launcher()
  print("trying to launch GetLevel now")
  GetLevel()
end
Luatic
  • 8,513
  • 2
  • 13
  • 34
  • Thanks for the explanation, I understand it now. I was implicitly telling it to look in the wrong place. I feared that code gets run "in order" and variables only start existing after their line of code gets executed. But I have like 120 functions and most of them are placed after they are called (because my usual format is to put help functions after the main functions that call them). Moving them all to the front would be quite convoluted, as many of them call each other. Is there some simple way to forward-declare them all? Can I do it in one line: local GetLevel, FindMax, GetBonus, ...? – JohnBig Feb 13 '22 at 18:14
  • I tried making them all local and adding 100 forward declarations, but somehow that crashes the script in ways even the syntax checker can't find anything with. (Which I never had before.) So for now it seems I'm stuck with the globals. – JohnBig Feb 13 '22 at 20:12
  • @JohnBig Yes, local (and upvalue) counts in Lua are pretty limited (to some hundreds, although I had something around 256 in mind?). That doesn't mean you have to resort to using globals though. The clean way would probably be to separate the script across multiple files to reduce the amount of locals per file (`do` ... `end` blocks or other lexical scopes would work as well). But if everything is as intertwined as you say, and many variables are set (which again indicates dirty code), you should consider using a table. – Luatic Feb 13 '22 at 20:28
  • @JohnBig Yes, you can declare multiple locals in "one line" using just that syntax: `local a, b, c`. – Luatic Feb 13 '22 at 20:28
  • @JohnBig To work around the local limitation, you can use a table as "scope". That might look as follows: `local myscope = {}; function myscope.MyFunc(...) end; function myscope.OtherFunc(...) myscope.MyFunc("yay") end` You can find [another example here](https://stackoverflow.com/a/38506505/7185318). – Luatic Feb 13 '22 at 20:30
  • @JohnBig Changing the environment differs across different Lua versions (5.2+ use `_ENV`, 5.1 uses `setfenv`) and is not something I would recommend unless you feel very proficient in Lua. You'd have to either localize the used actual globals (as in, the Lua library for example) or index the global table using a metatable (which comes with a different set of downsides). – Luatic Feb 13 '22 at 20:31
  • Thanks for the responses, I'll look into the solutions you mention. Unfortunately this video game I'm programming for does not have error reporting, so I'm effectively programming "blind". When something is wrong I may not notice, because it just crashes and I don't see what doesn't happen. Moving 100 functions so that there isn't one that calls on another that comes later would be difficult. I'd have to check them all manually. Plus it'd scramble the structure of my file. Adding more files is not possible because I'm appropriating a file from the game which I don't know how to launch. – JohnBig Feb 14 '22 at 05:14
  • 1
    I fixed a mistake in your forward declaration example. you need to remove the local keyword from the function definiton. otherwise the upvalue `GetLevel` remains nil. you'll declare a new local variable `GetLevel` that is not an upvalue to `Launcher`. it might also be worth adding a comment about the actual scope of the function as it looks like a global definition – Piglet Feb 14 '22 at 09:51
  • I noticed that forward declaring doesn't work for nested functions. I declare the sub-function to nil, execute it, then define it in the end of the parent function, just like I do in the non-nested situation. But at the point of execution the function is still nil. That makes sense, since variables in function are set "in order", otherwise functions wouldn't work linearly as they do. But how come this works on the base file level? Are those variables not run "in order"? When they are run, they are not defined yet either. – JohnBig Feb 14 '22 at 14:37
  • @Piglet thanks, I will add a comment explaining this. – Luatic Feb 14 '22 at 16:07
  • Making everything local turned out to be quite the hassle. Now I have to keep track of whether a function I add a call to is forward declared or not and comes behind or before. I just had to track down an error where I had added calls to a not forward declared function that came later. – JohnBig Feb 20 '22 at 13:37
  • @JohnBig we don't have your full script at hand. It might indeed be more reasonable to use a table here if you have many functions - either as environment or by explicitly indexing it. However, the root problem is probably that you'd need more files and scopes to neatly group together related functions in their own local (sub)scopes. – Luatic Feb 20 '22 at 17:29
2

So global functions can be "seen" from anywhere in the script, but local ones only from later in the code?

Yes, "global" means global.

You make something local if you don't want it to be visible outside of the script. A script should not expose something to the outside world unless the outside world needs to see it. Expose only interfaces, things the external code will actually talk to.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Why would being global make it "backward accessible"? In both cases the function is accessed from the same place. Is that because a call of a global function isn't actually a call of the function in my script, but of the version that function that gets put into the global table because? – JohnBig Feb 13 '22 at 16:59
  • @JohnBig: Because if a variable name isn't local, then it is global. That is, as the compiler reads the Lua text, it keeps track of which names are local. And if a name is used that doesn't match a local name the compiler has already seen, it must be a global one. Remember: the function only accesses what's in a name when it runs, but whether the name is local or global is defined at compile-time. So even though the global name doesn't have a value at the time it is identified as global, it *will* have a value when the function actually reads it. – Nicol Bolas Feb 13 '22 at 17:08
  • Ah ok, I was implicitly telling it to look in the wrong place. So it's not that all code gets run "in order" and variables only start existing after their line of code gets executed. It's all in memory and I can run it in any order, as long as I tell it to look for things in the right place. – JohnBig Feb 13 '22 at 17:35
2

The other guys already told you something about locals and globals in general. Let's take a look at your code and understand why this is not working.

local UseLevel = true
local Limit = 0

local function Launcher()
  print("trying to launch GetLevel now")
  GetLevel()
end

Above you defined three local variables. A boolean, a number and a function value. Launcher calls GetLevel which, assuming this is the only code you execute, is a global nil value. Because until that line you neither defined a local nor a global variable with that name. If you have never assigend a value to a variable, it is a nil value and if you have never declared that variable local it is a global nil value.

Then you define a local function named GetLevel. This is a new local variable. It is not the same GetLevel you used when you defined Launcher. Because that was a global variable.

local function GetLevel()
  print("GetLevel was launched")
  if UseLevel then
    Limit = 4
  else
    Limit = 5
  end
end

So when you later call Launcher it will still use the global nil value. Not the local which it doesn't know anything about.

The only way to make this work is to assign a function value to the very same variable that is being used in your Launcher definition. The scope does not matter as long it is the same reference. The only way to make this work with locals is to declare that local reference befor you define Launcher. it does not matter if you define GetValue befor or after Launcher as long as it has been defined when you call Launcher.

Piglet
  • 27,501
  • 3
  • 20
  • 43