Beyond Functions

Learn what functions really are and how much purpose they serve.

by aadenboy

Author Avatar

Beyond Functions

Learn what functions really are and how much purpose they serve.

Learning Target: I will learn how to use functions past basic calling, and use them in more complicated applications.

Difficulty: Moderate to Hard



What really are functions, anyways?


Functions in Lua (and other languages) are, believe it or not, values! When you do function foo() ... end, you're effectively saying foo = function() ... end. This is called syntactic sugar, and it's used in places you wouldn't expect.

Take for example the : operator. You might recognize this in lines such as event:Wait(). The : operator is just syntactic sugar for event.Wait(event), and is used to provide the Wait function the table (or object) it's in.

The += operator and it's siblings are also cases of syntactic sugar, and a += 1 is equivalent to a = a + 1.

With this in mind, we can find out that defining a function allows you to treat it like a variable.

function foo(bar) -- Create variable `foo` with a function as it's value.
	print(bar)
end

foo = nil -- Set `foo`'s value to `nil`.

foo(10) -- ERROR: attempt to call a nil value (global 'foo')
function a()
    return "Hello, world!"
end

local b = a -- `b` now does what `a` does.

print(a == b)    -- output: true
print(type(a))   -- output: function
print(a(), b())  -- output: Hello, world!, Hello, world!

This adaptability isn't only for us. The language itself uses it everywhere! Take for example pcall. pcall is also a function (only it uses C, and not Lua), and when we use it, we feed it our own function.

local success, err = pcall(function() -- Here, we're calling the `pcall` function, and feeding it a function. Notice how it's not named, it's only `function()`. This is a function being used as a value.
    error("Example error")
end) -- Closing off our function.
-- `pcall` returns two values after running.

-- more code...

Because pcall is a function, we can call it alone without defining new variables We can also place it into conditionals, equations, and more. Not sure what use we'd get out of that, though.

pcall(function() error("Example error") end)
-- this is valid

Another example would be the Connect function.

event:Connect(function() -- Same idea as `pcall`.
    -- code here
end)

The only difference here is that you can't call Connect on it's own, since it's part of the event itself.

Connect(function() --[[ code here ]] end) -- ERROR: attempted to call global 'Connect' (a nil value)

Practice 1

Assignment: Play around with functions, see what you can do!

Bonus: What are some other examples of things that use functions as values?


Alright, functions are values. What purpose does this really serve? If functions are values, we can place them anywhere, like regular values, and treat them like such.

This construct can help us make our code much nicer to look at. Here's an example:

if ability == "water" then
    print("Water used")
    -- code for water
elseif ability == "fireball" then
    print("Fireball used")
    -- code for fireball
elseif ability == "earthquake" then
    print("Earthquake used")
    -- code for earthquake
elseif ability == "tornado" then
    print("Tornado used")
    -- code for tornado
end

We can reformat the code to look like this:

local abilities = {
    water = function()
        print("Water used")
        -- code for water
    end,
    fireball = function()
        print("Fireball used")
        -- code for fireball
    end,
    earthquake = function()
        print("Earthquake used")
        -- code for earthquake
    end,
    tornado = function()
        print("Tornado used")
        -- code for tornado
    end
}

If you remember, function() foo ... end is equivalent to foo = function() ... end. This is what we're doing here, only it's inside of a table. If you don't like how this looks, you can always use your own syntactic sugar.

local abilities = {
         water = function() print("Water used")
                             --[[code for water]]      end
      fireball = function() print("Fireball used")
                             --[[code for fireball]]   end
    earthquake = function() print("Earthquake used")
                             --[[code for earthquake]] end
       tornado = function() print("Tornado used")
                             --[[code for fireball]]   end
}

Practice 2

Assignment: Add some more abilities to the table, make the functions do more!

Bonus: Figure out how to make it print an error message if the ability is invalid.


So? Why do that?


Why should I do this? Couldn't I just put the if statement into a function? This is a valid question. However, these function tables have more purpose than you think. Believe it or not, this specific construct is actually the reason for most (if not all) programming languages being able to do what they do!

If we want to call the tornado function specifically, we can do abilities.tornado(). The function can also return something, like the new health, stamina, or take in functions, like how much damage to deal, or a custom line to run.

Starting to seem familiar? Let's take a look at math.round(). You probably don't think much of this function, and that's with reason. But, the magic behind it is that you're doing exactly the same thing when calling that as you are with calling your ability.

math is a table. round is a function inside of that table. All functions that start with "math." are part of the math table. This extends further. When you create a new instance, you call Instance.new(). If you're noticing the pattern, new is a function in the Instance table, or object in this case. If you want to remove an instance, you use the :Destroy() function, which is a part of the instance itself.

Most of what you do functions this way. Any string function, math function, table function or instance function you do uses this construct.

Getting fancy, you can place your abilities table inside of a ModuleScript, and then require that every time you need the table in a function. It's amazing.


Practice 3

Assignment: Explore more cases of this construct in action. Try to make your own instance of this construct.

Bonus: The : operator requires a function to be in a table and to have a self or this argumet. Play around with this, how could it be helpful, and what can it be used for?


Bonus: Currying


Note: This is an advanced concept.

As a bonus, let's learn what currying is.

It's exact definition as per Wikipedia is:

[...] currying is the technique of translating a function that takes multiple arguments into a sequence of families of functions, each taking a single argument. In simple turns, we're taking a function, and splitting it's arguments into multiple calls.

Here, we have a function add that takes in two arguments a and b.

function add(a, b)
    return a + b
end

We can curry this function to split it's arguments up to multiple calls.

function curryAdd(a)
    return function(b)
        return a + b
    end
end

What this will allow us to do is to partially complete a computation for later use, as so:

function curryAdd(a)
    return function(b)
        return a + b
    end
end

local added = curryAdd(5)

print(added(3), added(7), added(1)) -- output: 8, 12, 6

Granted, this example isn't the best, but it's still a valid showcase of currying in action.

A more proper example would be an angle interpolator, as such:

function angleConstruct(from, to)             -- Create the curry.
    return function(at)                       -- Return the interpolator function configured to the `from` and `to` arguments provided.
       return from + (to - from) * (at / 100) -- The equation itself.
    end
end

local corner = angleConstruct(0, 90)  -- Create an interpolator configured to 0 through 90.
local wall = angleConstruct(0, 180)   -- Do the same for 0 through 180.
local circle = angleConstruct(0, 360) -- And 0 throuh 360.

-- Call the interpolators.
print("Corner middle is "..corner(50)) -- output: Corner middle is 45
print("Corner wedge is "..corner(15))  -- output: Corner wedge is 17.5
print("Wall ramp is "..wall(75))       -- output: Wall ramp is 135
print("Circle ends at "..wall(99))     -- output: Circle ends at 356.4

This is only a glimpse into the power of currying. Much more can be done with this. Also, you can do this:

function curryAdd(a)
    return function(b)
        return a + b
    end
end

print(curryAdd(5)(6)) -- output: 11

[HARD] Practice 4

Assignment: Come up with your own curry function. What is it for?

Bonus 1: Create a curry function that can be ran more than once, i.e func(?)(?)(?) returns something.

Bonus 2: Create a function that will create a curried version of the function. It's fine if you only make it handle two calls, i.e it only supports func(?)(?).


Thanks for reading!


This tutorial was made out of passion for the Lua programming language, and wanting others to know it's fullest potential.

I'm a self-taught programmer who has been using Lua for 5+ years now, and I'm still learning stuff I never knew before! Granted, I can make mistakes, and I most definitely do, but I'm proud with what I can complete.

If there's any mistakes I didn't catch, a complaint you have, or something you didn't understand, please by all means comment. I'm happy to answer!

-- aadenboy


View in-game to comment, award, and more!