E2 Guide: Lambdas - wiremod/wire GitHub Wiki

This articles covers in-depth usage of lambdas, E2's function objects. If you haven't already seen basic usage in the syntax guide, you should do so before reading ahead.

Closures

🔰 Beginner

Closures are a feature in programming that allow functions to pass local values to new functions. Consider the following scenario, where we have a function that inputs a number, and returns a function that uses that input, without being passed the input directly.

let MakeIncrementer = function(X:number) {
    return function() {
        X += 1
        return X
    }
}

let IncrementFromOne = MakeIncrementer(1)

print(IncrementFromOne()[number]) # prints 2
print(IncrementFromOne()[number]) # prints 3

So, what just happened here? We called MakeIncrementer which returns a function that uses the X parameter of MakeIncrementer. Because the scope of the returned anonymous function contained X, it was able to access it, much like accessing a global variable in a normal function.

You can depend on this behavior being isolated across two different calls of MakeIncrementer. Consider if we wanted two "incrementers" starting from different numbers. Conventional logic may think that one will overwrite the other, but you'll be pleasantly surprised to find that's not the case.

let IncrementFromZero = MakeIncrementer(0)[function]

print(IncrementFromZero()[number]) # Prints 1

let IncrementFromTen = MakeIncrementer(10)[function]

print(IncrementFromTen()[number]) # Prints 11
print(IncrementFromZero()[number]) # Prints 2
print(IncrementFromTen()[number]) # Prints 12

So, what we see here is that if we define a function, all the local variables (called upvalues) inside that function can be used as though they were its own, even if they aren't defined globally or in the function itself.

Closure Warnings

Be careful when using objects, tables, and arrays within a closure. They won't act like you expect them to do. Consider the following example, where we pass a table and then modify it.

let Closure = function(T:table) {
    return function() {
        print(T[1, number])
    }
}

let Table = table(12345, 67890)

let Func1 = Closure(Table)[function]

Func1() # Prints 12345

Table[1] = 100

let Func2 = Closure(Table)[function]

Func1() # Prints 100
Func2() # Prints 100

As you see, both Func1 and Func2 are outputting the modified Table, despite Func1 being created before it was modified. This is because tables are passed by reference, which means that when you call a function with a table, you're getting the same table, not a copy of it. Any modifications to the table inside the function are applied to the same table, and any closure that uses that table is using that same table, too. What can be done to fix this? Well, we can simply clone the table or create a new table.

let Table = table(12345, 67890)

let Func1 = Closure(Table)[function]

Func1() # Prints 12345

Table = table(100) # Creates a new table and replaces *Table*'s reference to it

let Func2 = Closure(Table)[function]

Func1() # Prints 12345
Func2() # Prints 100

Example: Chat Command Look-up Table

🟢 Easy

If you're familiar with using look-up tables in E2, you can use function lambdas to create a chat command processor that you can easily extend and modify.

First, let's start with making our table to store functions, the command "keyword", and register the chat event. Inside the event, I'll put some code that checks if the Message starts with the keyword. Since it's only one character, I can cheat here by simply using array indexing to get the first character.

@strict
const Functions = table()
const Keyword = "/"

event chat(Player:entity, Message:string, _:number) {
    if(Message[1] == Keyword) {

    }
}

Now inside of the if statement, I'll put this code which will split the message after the keyword and also split apart the command from the rest of the arguments. Then, I'll have another if statement to check if the command exists inside the function table. If it does, I'll run it and pass the leftover array of arguments. I'll also add a warning if the user inputs a bad command, which isn't necessary but may be nice.

let Args = Message:sub(2):explode(" ") # Skip the first character ("/") and split the string into an array
let Command = Args:shiftString():lower() # Removes and returns the first element, and shifts everything down to compensate
if(Functions:exists(Command)) {
    Functions[Command, function](Args)
} else {
    print("Unknown command: " + Command)
}

If we were to run this now, we'll just keep getting the unknown command error because, of course, we haven't made any commands yet. So, let's see how simple it is to add new ones. I'll define some basic ones. At the top of our code, right after where we initialized Functions, we can add new functions just like so:

Functions["ping"] = function(_:array) {
    print("Pong!")
}

Functions["hurt"] = function(Args:array) {
    let Name = Args[1, string]
    let Player = findPlayerByName(Name)
    if(Player) {
        let Damage = Args[2, string]:toNumber()
        try {
            Player:takeDamage(Damage)
        } catch(_:string) {
            print("Damage is not allowed!")
        }
    } else {
        print(format("Couldn't find player %q", Name))
    }
}   

Important

The array argument is required even when it's not used. Your return type and function parameters need to be consistent for this to work, or you'll run into an error.

Now with all this together, we should be able to run our ping and hurt commands. You can add more commands just by adding a new function to the table.

Full code
@strict
const Functions = table()
const Keyword = "/"

Functions["ping"] = function(_:array) {
    print("Pong!")
}

Functions["hurt"] = function(Args:array) {
    let Name = Args[1, string]
    let Player = findPlayerByName(Name)
    if(Player) {
        let Damage = Args[2, string]:toNumber()
        try {
            print("Trying to hurt")
            Player:takeDamage(Damage)
        } catch(_:string) {
            print("Damage is not allowed!")
        }
    } else {
        print(format("Couldn't find player %q", Name))
    }
}  

event chat(Player:entity, Message:string, _:number) {
    if(Message[1] == Keyword) {
        let Args = Message:sub(2):explode(" ") # Skip the first character ("/") and split the string into an array
        let Command = Args:shiftString():lower() # Removes and returns the first element, and shifts everything down to compensate
        if(Functions:exists(Command)) {
            print("Calling " + Command)
            Functions[Command, function](Args)
        } else {
            print("Unknown command: " + Command)
        }
    }
}
⚠️ **GitHub.com Fallback** ⚠️