-
Notifications
You must be signed in to change notification settings - Fork 341
E2 Guide: Lambdas
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.
Introduction
• Functions as Data
• Closures
Examples
• Chat Command Look-Up Table
The ability to create variables and store things in them is a vital component of many programming languages. The ability to create functions that take arguments is also rather paradigm. But what if you encounter a scenario where you yearn to do both in the same feature?
If you want to do any of the following...
- Store a function in a variable
- Pass a function to another function
- Store a function on a table/entity
- Store state inside a function
Lambdas are your solution!
A function is just a group of code that you can run whenever you want. If you wanted to create a function that printed out "hello" whenever you ran it, you'd create it like this:
function() {
print("Hello")
}
This is pretty useless on its own, though, because it creates a function and doesn't store it anywhere, so we usually assign that to a variable.
MyFunction = function() {
print("Hello")
}
Now, you can use function call syntax on MyFunction to call the function it's assigned to.
MyFunction() # prints "Hello"
That seems cool and all, but since MyFunction is technically a variable, that means we can store its value to multiple other places.
MyOtherFunction = MyFunction
MyOtherFunction() # prints "Hello", too!
MyTable[1] = MyFunction
MyTable[1, function]() # Does what you'd expect
And finally, since it acts like a variable, we can also pass it to other functions, like timer
.
timer(1, MyFunction) # Prints "Hello" after 1 second
Now, all of that is to say, we could also just skip the part about doing MyFunction =
and pass the function literal directly to the timer
function.
timer(1, function() {
print("Hello")
}) # This code is equivalent to MyFunction, so it's functionally the same
Lambdas are just another name for "functions without a name", like we have demonstrated here.
Because of how E2 works, you should know that global variables are accessible in any scope. That means you can modify global variables in functions, too.
@outputs Out
timer(1, 0, function() { # The second argument here specifies repetitions. 0 means it repeats forever.
Out++ # Out will be incremented by 1 each second
})
And because functions can be made anywhere, you have free reign to place them right next to the code that's most relevant to them.
@inputs In
@outputs Out
event input(_:string) {
if(!In) { exit() } # Only trigger if In is active
if(!Out & !timerExists("auto off")) {
Out = 1 # Turn on the output
timer("auto off", 1, function() { # We use a named timer to avoid overlapping timers
Out = 0 #Turn off the output after 1 second
})
}
}
If you wanted a single timer to run most of your code at a specific interval, you'd simply encapsulate all of that code in a timer.
@name Mario Party Dice roller (but not RNG)
@inputs Stop
@outputs Out
@trigger none
@strict
holoCreate(1)
Out = 0
let Value = 0
timer("main", 0.05, 0, function() { # Runs every 0.05 seconds infinitely
Value = (Value + 1) % 100
Out = (Value % 10 + 1) # Value rotates from 1 - 10
let Ang = Value / 100 * 360
let Color = hsv2rgb(Ang, 1, 1)
holoColor(1, Color)
let C = Ang - 180
holoAng(1, ang(C, -C, 0))
})
event input(InputName:string) {
if(InputName == "Stop") {
if(Stop) {# Stops the timer when the input is on
timerPause("main")
} else {
timerResume("main")
}
}
}
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.
function function makeIncrementer(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
with an argument ofX = 1
-
makeIncrementer
returned a function that needs the enclosing scope it was created in to work (it relies onX
), hence the name "closure". - When we call
IncrementFromOne
, it increments the capturedX
and returns the result of that. - Each subsequent call,
IncrementFromOne
remembers whatX
was last time and returns increasing values accordingly!
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)
print(IncrementFromZero()[number]) # Prints 1
let IncrementFromTen = makeIncrementer(10)
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.
Be careful about what kinds of objects you try to capture with closures. If they aren't primitives (numbers or strings), you'll end up capturing a reference to the object rather than a true copy of it! This may lead to maddening "wtf is happening" kinds of behavior!
function function makeEvilIncrementer(T:table) {
return function() {
T["x", number] = ["x", number] + 1
return T["x", number]
}
}
let NumberInATable = table("x" = 1)
let EvilIncrementer1 = makeEvilIncrementer(NumberInATable)
let EvilIncrementer2 = makeEvilIncrementer(NumberInATable)
print(EvilIncrementer1()[number]) # Prints 2
print(EvilIncrementer2()[number]) # Prints 3 (????)
NumberInATable["x", number] = -1
print(EvilIncrementer2()[number]) # 0 (???????)
The closure's upvalue is only a reference to the original NumberInATable
, and NOT a reference to a copy. For non-primitive values, making a copy is your responsibility!
So how do we fix the above behavior?
Tables have a convenient method called :clone()
that makes copying them easy:
function function makeLessEvilIncrementer(T:table) {
T = T:clone() # redefine the upvalue as a copy of itself
return function() { # now carry on as usual
T["x", number] = ["x", number] + 1
return T["x", number]
}
}
Careful using this method as a bandaid fix though—deep copying a table can get expensive fast!
🟢 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] = function(_:array) {
print("Pong!")
}
Functions["hurt", function] = 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)
}
}
}
Please do not alter the e2 docs ...
pages manually.
They are autogenerated from the E2Helper. In the future, this will hopefully be its own dedicated website or tool.
Basic Features: core, debug, number, selfaware,
string, timer
🌎 World: angle, color, find, ranger, sound,
🔣 Math: bitwise, complex, matrix, quaternion, vector, vector2/4
📦 Entities: bone, constraint, egp, entity, hologram, npc
👨 Players: chat, console, player, weapon
📊 Data storage: array, files, globalvars, serialization, table
💬 Communication: datasignal, http, signal, wirelink,
❓ Informational: gametick, serverinfo, steamidconv, unitconv
Disabled by default: constraintcore, effects, propcore, remoteupload, wiring
Wire-Extras (repo): camera, ftrace, holoanim, light, stcontrol, tracesystem
Expression 2 ⚙️
- Syntax 🔣
- Directives 🎛️
- Editor 🖥️
- Ops 📊
- Learning & Getting Help 📚
- Events 🎬
- Find Functions 🔍
- Physics 🚀
- EGP Basics 📈
- Lambdas λ
- Lambda Timers λ⏲️
- Tips & Tricks 📘
Click To Expand
- 🟥 SPU
- 🟥 Address Bus
- 🟥 Extended Bus
- 🟥 Plug/Socket
- 🟥 Port
- 🟥 Transfer Bus
- 🟩 GPU
- 🟥 Dynamic Memory
- 🟥 Flash EEPROM
- 🟥 ROM
- 🟧 Beacon Sensor
- 🟧 Locator
- 🟧 Target Finder
- 🟧 Waypoint
- 🟥 XYZ Beacon
- 🟩 CPU
- 🟩 Expression 2
- 🟩 Gates
- 🟥 PID
- 🟧 CD Disk
- 🟥 CD Ray
- 🟧 DHDD
- 🟥 Keycard
- 🟥 RAM-card
- 🟧 Satellite Dish
- 🟧 Store
- 🟧 Transferer
- 🟥 Wired Wirer
- 🟧 Adv Entity Marker
- 🟧 Damage Detector
- 🟧 Entity Marker
- 🟧 GPS
- 🟧 Gyroscope
- 🟥 HighSpeed Ranger
- 🟧 Laser Pointer Receiver
- 🟥 Microphone
- 🟧 Ranger
- 🟧 Speedometer
- 🟧 Water Sensor
- 🟧 7 Segment Display
- 🟥 Adv. Hud Indicator
- 🟧 Console Screen
- 🟧 Control Panel
- 🟧 Digital Screen
- 🟧 EGP v3
- 🟧 Fix RenderTargets
- 🟥 GPULib Switcher
- 🟧 Hud Indicator
- 🟧 Indicator
- 🟧 Lamp
- 🟧 Light
- 🟧 Oscilloscope
- 🟧 Pixel
- 🟧 Screen
- 🟧 Sound Emitter
- 🟧 Text Screen
- 🟩 Cam Controller
- 🟧 Colorer
- 🟧 FX Emitter
- 🟧 HighSpeed Holoemitter
- 🟧 HoloEmitter
- 🟧 HoloGrid
- 🟥 Interactable Holography Emitter
- 🟥 Materializer
- 🟥 Painter
- 🟧 Adv. Input
- 🟧 Button
- 🟧 Constant Value
- 🟥 Door Controller
- 🟧 Dual Input
- 🟧 Dynamic Button
- 🟧 Eye Pod
- 🟧 Graphics Tablet
- 🟧 Keyboard
- 🟥 Lever
- 🟧 Numpad
- 🟧 Numpad Input
- 🟧 Numpad Output
- 🟧 Plug
- 🟧 Pod Controller
- 🟧 Radio
- 🟧 Relay
- 🟧 Text Receiver
- 🟧 Two-way Radio
- 🟧 Vehicle Controller
- 🟥 Door
- 🟥 Adv. Dupe. Teleporter
- 🟥 Buoyancy
- 🟧 Clutch
- 🟧 Detonator
- 🟧 Explosives
- 🟧 Explosives (Simple)
- 🟥 Forcer
- 🟩 Freezer
- 🟧 Gimbal (Facer)
- 🟧 Grabber
- 🟧 Hoverball
- 🟧 Hoverdrive Controller
- 🟥 Hydraulic
- 🟧 Igniter
- 🟧 Nailer
- 🟩 Prop Spawner
- 🟥 Servo
- 🟥 Simple Servo
- 🟧 Thruster
- 🟥 Touchplate
- 🟥 Trail
- 🟩 Turret
- 🟩 User
- 🟥 Vector Thruster
- 🟥 Vehicle Exit Point
- 🟧 Weight (Adjustable)
- 🟧 Weld/Constraint Latch
- 🟥 Wheel
- 🟥 Wire Magnet
- 🟥 Wired Npc Controller
- 🟧 Debugger
- 🟥 GUI Wiring
- 🟥 Multi Wire
- 🟧 Namer
- 🟥 Simulate Data
- 🟩 Wiring
- 🟥 Beam Reader
- 🟥 Implanter
- 🟥 Reader
- 🟥 Target Filter
- 🟥 User Reader
Gates 🚥
Click To Expand
TBD