Understanding IIFE (Immediately Invoked Function Expression)

Introduction

IIFE stands for Immediately Invoked Function Expression. It is a design pattern that the program will invoke the functions once they are defined. It is also known as the Self-Executing Anonymous Function design pattern, but it is theoretical. There are some differences we should know when working with this type of stuff beforehand, and 2 of them are declarations and expressions. But for this use case, we will see function differences.

Function Expression and Function Declaration

A function expression can be used as an IIFE (Immediately Invoked Function Expression), which runs as soon as it is defined. Function expressions often do not have a name, and they use the name where they are stored to invoke. For example:

--!strict
local foo: () -> () = function()
    -- // ...
end

This function could be known as an anonymous function. As opposed to this, the declaration of a function contains a name and body and stills around on our program, and we can use it after it declares itself. For example:

--!strict
local function foo(): nil
    -- // ...
end

Note that you cannot use function expressions before they are defined, formally known as hoisting.

--!strict
foo();

function foo(): nil
    -- // ...
end

But in an expression:

--!strict
foo() -- // Tried to access a global foo

local foo: () -> () = function()
    -- // ...
end

The difference between Function Expressions and Function Declarations

Function expression and function declaration are different. The main difference between a function expression and a function declaration is the function name, which can be omitted in function expressions to create anonymous functions. (Luau interprets all of this data differently. But don’t pay attention to that yet!). Why do we need to know about this type of stuff when discussing IIFE? When we use this design pattern in our program, we use function expressions, not declarations. You might wonder how.

Well, first, we need to use the () to delimit the scope of the function, and it lets us run it since it got declared from there. It is like using coroutine.wrap()() since it returns a function, but this is not returning one. Anyway, let us see an example:

(function() ... end)()

Just like this, we have already created our first function that will be invoked straight away from its creation! But in a more use-case scenario, we could do this:

(function(message) print(message) end)("Hello, world!")

And this should give us the output, Hello, world!, which is perfect!

Examples

The examples I found below are from Wikipedia. But I rewrote the examples in Luau.

Evaluation Context

A lack of block scope means that variables defined inside (for example) a for loop will have their definition “hoisted” to the top of the enclosing function. Evaluating a function that depends on variables modified by the outer function (including by iteration) can be difficult. We can see this without a loop if we update a value between defining and invoking the function.

--!strict
local v: number, getValue: () -> number;
v = 1;
getValue = function() return v end;
v = 2;
getValue(); -- // 2

While the result may seem obvious when updating v manually, it can produce unintended results when getValue() is defined inside a loop.

Here after the function passes v as an argument and is invoked immediately, preserving the inner function’s execution context.

--!strict
local v: number, getValue: () -> number;
v = 1;
getValue = (function(x) 
  return function() return x end 
end)(v);
v = 2;
getValue(); -- // 1

Establishing Private Variables and Accessors

IIFEs are also useful for establishing private methods for accessible functions while still exposing some properties for later use. The following example comes from Alman’s post on IIFEs.

--!strict
-- // "counter" is a function that returns an object with properties, which in this case are functions.
local counter: any = (function(): any
    local i: number = 0;

    return {
        get = function() 
            return i;
		end,
        set = function(val: number)
            i = val;
		end,
        increment = function()
			i += 1;
            return i;
		end
    };
end)();

-- // These calls access the function properties returned by "counter".
counter.get(); -- // 0
counter.set(3); -- // 3
counter.increment(); -- // 4
counter.increment(); -- // 5

Now you know this information, and you will keep it forever in your brain. I hope you guys like it! Happy coding, and always practice this type of stuff on your own!

(This post is inspired by @SiriusLatte)

12 Likes

thanks a bunch for the insight given amazing tutorial

1 Like

I always felt that IIFEs where only really helpful when there was dedicated syntax-sugar for it like in JavaScript. In Lua, they just come off as an eyesore.

At best, you could just use do blocks to perform basically the same thing for the counter example.

local Counter do
	local I: number = 0
	
	Counter = {
		Get = function()
			return I
		end,
		...
	}
end

Apparently Python doesn’t have this syntax, I got used to it in Roblox already. Glad to see others see its usefulness

The do block solution is kinda annoying though because if you wanna change the name of the Counter variable, you have to do it in multiple places within its definition context. With an immediate function, you don’t have to do Counter = again, you can just use return.

do block:

local x do
    local y = 5
    x = y * 3
end

IIFE:

local x = (function()
    local y = 5
    return y * 3
end)()

One thing to note here though is if you use an upvalue inside the lambda the function immediately becomes impure and can’t be cached.

Generally speaking, more modern languages like Rust favor block expressions over IIFE. It is more likely that the luau compiler will later optimize scoped assignment (i.e. remove the LOADNIL and just load the register directly) because it is a more common idiom.

I think it’s a bit strange to talk about IIFEs in the context of Luau. IIFE is a distinctly JavaScript pattern to solve a pain-point with how scopes worked. So, I don’t think it makes much sense to apply it to Luau (we simply don’t have the same problem).

The missing context: JavaScript

Back in the day, there were no modules. So, when you included a JavaScript file, they all worked in the scope. Everything was public by default.

var was the only way to declare variables, but it only was scoped to functions. Meaning:

while (condition) {
    var a = 10
    // ...
}

console.log(a) // This will actually log 10!

It gets worse if you want to have private variables for your module (JQuery, let’s say) or don’t want to clutter the global scope with a bunch of variables (which could potentially conflict with others and cause an error!). To fix that, we’d have to do this:

function b() {
    while (condition) {
        var a = 10
        // ...
    }
}

b()

console.log(a) // This will do undefined, yay!

There’s still a b that leaks out, but fortunately functions are expressions like in Luau; hence IIFEs!

;(function() {
    while (condition) {
        var a = 10
        // ...
    }
})();

And if you need to export stuff out (like a counter), pass the global scope inside (window in the browsers):

;(function(global) {
    var Counter = {
        // ...
    }
    global.counter = Counter
})(window); 

// Or do logic to make it cross-platform (e.g. NodeJS is different)
// and usable with different loaders.
// (JS doesn't have require() like in Luau, 
// so there's several versions of it in JS) 

Why not in Luau?

But, unlike JavaScript, in Luau, we don’t have that difficulty. We have a module system, we don’t have weird hoisting rules, we don’t have weird scoping rules (a la var). We have options, and IIFEs isn’t idiomatic (the standard way) at all in Luau. You’ll get funny looks for doing it that way (and I agree with Magma, they’re an eyesore).

One of the idiomatic (standard way, as MagmaBurns said) is a do end. Practically, we recreated that with functions in JavaScript, but unlike functions, it has no cost of creating/calling and is designed for this issue.

Another idiomatic way is to actually pull that code out into its own module instead. Personally, if your do end that initializes one variable takes more than 10–15 lines, then it probably should be its own module. That way, the focus stays on the main script instead of the private details of how X is done.

An example / side-tangent Another thing to note: the counter example you used could just be a proper function that you can invoke. You probably want more than one counter anyhow.
local function MakeCounter()
    local i: number = 0;

    return {
        get = function() 
            return i;
		end,
        set = function(val: number)
            i = val;
		end,
        increment = function()
			i += 1;
            return i;
		end
    }
end

local counter = MakeCounter()

Taking the idea of moving it to its own module:

-- Stays focused on the purpose of the main script!
local MakeCounter = require(path.to.Counter)
local counter = MakeCounter()
1 Like