Scope within Lua

Far too often, while opening up a LÖVE file that someone posts on the forums, I see the telltale signs of someone who does not understand the concept of scope. In their player.lua file, they will be referring directly to some variables inside their wall.lua file, which itself refers directly to some tables within the main.lua file.

Granted, there are some instances when polluting the global scope has a few benefits, but you need to truly understand what it means before you can safely do so.

About Me

My name is Michael Kosler, and I'm a college student currently attending Texas A&M University, earning a bachelor's in Computer Science. I previously got a BA in Mathematics from Texas A&M University, and taught high school mathematics for about four months (couldn't even last a year :)). I'm hoping to break into the game industry professionally, and have been using Lua and LÖVE since 2011 to help build out my online portfolio.

What is scope?

Ripped from Wikipedia:

In computer programming, a scope is a context within a computer program in which a variable name or other identifier is valid and can be used, or within which a declaration has effect.

While Wikipedia often gets the definition technically right, it also assumes you probably know what scope is before you begin reading up on scope - a backwards proposition.

Scope, in the most general case, can be distilled down to one question: "Who knows I exist?" This is an important question to ask in programming, because rarely does a program not need to communicate with either other programs or other parts of itself. If you share everything, then you might end up clashing with other parts of your program that need similarly named variables.

Globe Trekking

The global scope is probably the simplest to understand. If I have a variable within the global scope, then it is visible to the entire program. For example, consider two files: a.lua, and b.lua.

a.lua

    -- x is defined in the global scope
    x = 10

    -- foo is defined in the global scope
    function foo()
      -- Since x is global, the function foo can refer to it
      x = x + 5
      print(x)
    end

b.lua

    -- Bring the global scope from a.lua into b.lua
    require 'a'

    -- bar is defined in the global scope
    function bar()
      -- This x is still referring to the x we created in a.lua
      x = x + 10
      foo()
    end

    bar()

Running b.lua prints 25 onto the screen.

I only buy Local Scope

Local scope refers to pretty much all other situations of scoping. Generally, however, to be in the local scope usually means that a variable is visible only to those other variables that share the same scope. I will get to some examples on what local scope looks like in code in a bit, but first I have to steer you away from a common problem.

If you are familiar with other programming languages, then this so far has felt fairly familiar to you. Local scope, however, is where Lua starts acting differently. Consider the following program:

    x = 10

    function foo()
      y = 20
    end

    function bar()
      print(x, y)
    end

    foo()
    bar()

In most other languages, this will give you an error. Why? Because in most other languages, the variable y, defined within the function foo, is local to that function, so when bar tries to print y, it has no understanding as to what y is, and balks.

Lua, on the other hands, happily prints 10, 20. Why? Because, in Lua, all variables are, by default, global. Why would it do that? Ask Dr. Ierusalimschy. All we care about is how we take advantage of local scoping.

Turns out its fairly easy: just append local to any variable that you wish that be inside its local scope. Given that, let us fix the above code snippet to work like other programming languages:

    x = 10

    function foo()
      local y = 20
    end

    function bar()
      print(x, y)
    end

    foo()
    bar()

Now, while this does not quite give us the error we would get in many other programming languages, Lua prints 10, nil, showing that y has no current meaning within the scope of bar.

We can extend this idea of local scope between files as well. Suppose I have three files:

player.lua

    -- some other player functions

    function draw(o)
      love.graphics.setColor(o.color)
      love.graphics.rectangle('fill', o.x, o.y, o.width, o.height)
    end

enemy.lua

    -- some other enemy functions

    function draw(o)
      love.graphics.setColor(o.color)
      love.graphics.circle('fill', o.x, o.y, o.radius)
    end

main.lua

    require 'player'
    require 'enemy'

    local p = {
      x = 10,
      y = 10,
      width = 20,
      height = 50,
      color = { 0, 255, 0 },
    }

    local e = {
      x = 50,
      y = 50,
      radius = 15,
      color = { 255, 0, 0 },
    }

    function love.draw()
      -- Which one is which?
      draw(p)
      draw(e)
    end

Clearly, I want different functions for drawing my player and my enemies. But, both the draw function in player.lua and the draw function in enemy.lua pollute the global scope. So, which draw was used? It actually depends on the order of the require statements, so in our case, the draw in enemy.lua is the draw within the main.lua, so we get an error when we call draw(p), since p does not have radius.

The fix is fairly easy:

player.lua

    -- declare draw as local
    local function draw(o)
      love.graphics.setColor(o.color)
      love.graphics.rectangle('fill', o.x, o.y, o.width, o.height)
    end

    -- return the local version of draw
    return draw

enemy.lua

    -- declare draw as local
    local function draw(o)
      love.graphics.setColor(o.color)
      love.graphics.circle('fill', o.x, o.y, o.radius)
    end

    return draw

main.lua

    -- Assign the return value of the two files to variables
    local playerDraw = require 'player'
    local enemyDraw = require 'enemy'

    local p = {
      x = 10,
      y = 10,
      width = 20,
      height = 50,
      color = { 0, 255, 0 },
    }

    local e = {
      x = 50,
      y = 50,
      radius = 15,
      color = { 255, 0, 0 },
    }

    function love.draw()
      playerDraw(p)
      enemyDraw(e)
    end

Why should I care?

Think of scope as a directory or folder inside your computer. If you exclusively use the global scope, then your computer has simple one directory, with all files shoved inside. In this flat structure, anytime you attempt to save a new file, you would have to be on guard against accidentally overwriting a previously saved file. Luckily, most modern operating systems warn you before you accidentally save over some previous work. Lua, and most other programming languages, do not give you that extra level of protection.

What are you leaving out?

Lua provides quite a few interesting tricks to meddle with the scope of the language. If you are interested, here are a few advanced topics on scope within Lua:

Comments

substitute541's picture

Great article! I try to think of scope as boxes (or squares) nested to each other. Only items either inside the bigger boxes or inside the same box can be accessed. Global variables can be outside the nested boxes, and can be accessed by any box.