Skip to main content

Context Parameters

Easel has an unusual feature called Context. If a function call is missing certain parameters, the Easel compiler looks for variables with the same names as the function parameters in the surrounding code, and automatically wires them up for you. There are particular rules around this to stop this causing unexpected side-effects.

Games are often made through a process of iteration and experimentation. Used to its greatest effect, context can greatly reduce the amount of boilerplate code you need to write, making your code smaller, lighter and easier to experiment with.

Context is resolved at compile time and so is equally efficient as passing in parameters manually. Missing context parameters will be detected at compile-time and reported as errors. One of the concerns people may have about implicit context is unintended consequences, but because Easel's context parameters are verified at compile time, you can be more confident that your code will work as expected.

Basics

When a function is missing context parameters, the Easel compiler will look for context variables that have the same name as the missing parameters, and use them to fill in the missing parameters. Not every variable and not every parameter is eligible for this treatment, but we will discuss the exceptions and the rules later.

The most straightforward way to declare a context variable is with a use statement:

fn this.Example() {
use color=#ff8800 // declare a context variable
Spark(radius=5) // What color is the spark? It is #ff8800 because of context
}

Context parameters

Not every function parameter may be filled from context. Parameters that can be filled from context are called context parameters. These are declared in two ways:

  • Parameters declared inside square brackets ([ and ]),
  • Or the function's subject parameter, may be filled from context.

Example of the square bracket notation:

// `color` and `radius` may be filled from context, but `name` may not be
fn FunctionWithContextParameters(name, [color, radius]) { /* ... */ }

fn Example() {
// declare a variable `color` will provide its value to any function that needs it
use color=#ff8800

FunctionWithContextParameters("Augustus", radius=5) // color not explicitly given
// Equivalent to:
// FunctionWithContextParameters("Augustus", radius=5, color=#ff8800)
// Easel implicitly passes in `color` from context
}

The subject parameter is also always found from context too.

// `ship` is the subject parameter, and therefore is also a context parameter
fn ship.ActivateWarpDrive { /* ... */ }

fn Example() {
use ship = Spawn

ActivateWarpDrive
// Equivalent to: ship.ActivateWarpDrive
}

Context variables

When Easel is looking for a value for a context parameter, only context variables are considered. Not every variable is a context variable.

As was shown earlier, the most straightforward way to declare a context variable is with a use statement. The use statement declares a context variable only within the current block (between the braces { and }). It is possible to declare multiple variables at once using a single use statement.

fn FunctionWithContextParameters(name, [color, radius]) { /* ... */ }
fn Example() {
use color=#ff8800, radius=5 // declare multiple context variables at once

FunctionWithContextParameters("Augustus")
// Equivalent to:
// FunctionWithContextParameters("Augustus", color=#ff8800, radius=5)
}

Variables declared with use cannot be changed - they are immutable. Any attempt to assign a value to a variable declared with use will result in a compile-time error.

fn Example() {
use color=#ff8800
color = #00ff00 // compile-time error
}

To improve the expressiveness of the language, certain other variables are also automatically declared as context variables.

Function parameters are context variables

All parameters in the function signature are context variables. This is done because they are so commonly used as context variables that it would be very verbose to declare them with use.

fn FunctionWithContextParameters(name, [color, radius]) { /* ... */ }
fn Example(radius) { // radius is a context variable
use color=#ff8800
FunctionWithContextParameters("Augustus")
// Equivalent to:
// FunctionWithContextParameters(name=, color=, radius=)
}

Subblock parameters are also considered context variables. This is also done because they are so commonly used as context variables.

fn Example() {
Spawn ship { // ship is a context variable
ActivateWarpDrive
// Equivalent to: ship.ActivateWarpDrive
}
}

fn ship.ActivateWarpDrive { /* ... */ }

Subject parameter defines this

The subject parameter of a function is also implicitly declared as a context variable named this. This is useful because many functions take this as their subject parameter, and so this reduces the amount of boilerplate code you need to write for this very common parameter. Learn more about the special variable this here.

fn ship.RedirectPowerToShields() { // `ship` is a context variable, also given the name `this`
ImageSprite(@shield.svg, radius=10)
// Equivalent to: this.ImageSprite(@shield.svg, radius=10)
}

Subblocks may add implicit context variables

It is possible for functions to give their subblocks implicit context variables. This can help reduce boilerplate and improve expressiveness, but should be used sparingly as their effects can be completely hidden from the caller and so can be hard to see and debug.

Easel's built-in functions only ever use this technique for two variables - this and ui, as they are such commonly repeated patterns that Easel would be very verbose without this feature. In general, you should only use this feature for this and ui as well, unless you have a very good reason to do so otherwise.

Subblock parameters are declared between two pipe characters | in the function signature. To declare a subblock parameter as an implicit context variable, prefix it with the use keyword.

Below is an example of a function that spawns an entity that represents a grabbing action that lasts for 5 seconds. By convention, in Easel whenever a function spawns a new entity, it sets this to the new entity in the context of its subblock. See this to learn more about the most important this context variable.

// `use this` makes `this` an implicit context variable in the subblock of SpawnGrab
pub fn SpawnGrab() |use this = grab| {
Spawn grab {
delve(grab)
await Tick(5s)
Expire
}
}

pub fn Example() {
SpawnGrab {
// This subblock contains an implicit context variable named `this`
ImageSprite(@grab.svg, radius=10)
// Equivalent to: this.ImageSprite(@grab.svg, radius=10)
// Note that we never give `this` a name within this block, it is implicit
}
}

Functions that work with the ui variable commonly declare ui as an implicit context variable. Otherwise, the code would be needlessly verbose. Below is an example of a function that creates a custom UI component, and adds an implicit ui context variable to its subblock.

// The `use ui` makes `ui` an implicit context variable in the subblock of DigitCard
fn DigitCard(heading=null, [ui]) |use ui| {
Panel(align=Align:Center, gap=0, expand=1) {
%(heading)
P(fontSize=4, bold=true) {
delve()
}
}
}

pub fn player.Example() {
Content {
DigitCard(heading="Games Played") { %(player.NumLifetimeGames) }
DigitCard(heading="Outlasts") { %(player.NumLifetimeOutlasts) }
DigitCard(heading="Wins") { %(player.NumLifetimeWins) }
}

// This would be much more verbose with no context variables:
Content ui { // `ui` context variable would normally be implicit
DigitCard(heading="Games Played", ui=) { ui.%(player.NumLifetimeGames) }
DigitCard(heading="Outlasts", ui=) { ui.%(player.NumLifetimeOutlasts) }
DigitCard(heading="Wins", ui=) { ui.%(player.NumLifetimeWins) }
}
}

See UI to learn more about the ui context variable.

Non-context variables

Variables declared with let or const are not context variables.

fn FunctionWithContextParameters(name, [color, radius]) { /* ... */ }
fn Example() {
let color = #ff8800 // not a context variable
use radius = 5 // context variable
FunctionWithContextParameters("Augustus")
// Equivalent to:
// FunctionWithContextParameters("Augustus", radius=5)
}

Also, the loop variable in a for loop is not a context variable, unless it is explicitly declared with use.

fn FunctionWithContextParameters(name, [color?, radius?]) { /* ... */ }
fn Example1() {
for color in [#ff8800, #00ff00] { // `color` is not a context variable
FunctionWithContextParameters("Augustus")
// Equivalent to:
// FunctionWithContextParameters("Augustus")
}
}

fn Example2() {
for use color in [#ff8800, #00ff00] { // `color` is a context variable because of `use`
FunctionWithContextParameters("Augustus")
// Equivalent to:
// FunctionWithContextParameters("Augustus", color=)
}
}

Enclosing scopes only

Only variables in the current scope or in enclosing scopes are considered candidates when searching for a parameter from context.

fn FunctionWithContextParameter([name?, age?]) { }
fn Example() {
use name="Augustus", age=25
Spawn ship {
FunctionWithContextParameter
// Equivalent to: FunctionWithContextParameter(name="Augustus", age=25)
// Both `name` and `aged` passed in from context,
// even though they are in an enclosing scope, not the current scope
}
}

When a function calls another function, it is not an enclosing scope and so context variables do not leak from one function into another.

fn FunctionWithContextParameter([name?, age?]) { }
fn Example() {
use name = "Augustus" // add `name` to context
InnerFunction
}
fn InnerFunction {
// `name` is not in context here,
// even if we are called from `Example`

use age=25
FunctionWithContextParameter()
// Equivalent to: FunctionWithContextParameter(age=25)
}

That means, in general, the only variables that can be passed in from context are the ones you can see. There is a limited exception to this rule, detailed later on this page. This is important as it avoids unexpected bugs. It is not possible for functions far away in the call stack to cause unanticipated side effects.

Recap

Context is a powerful feature that allows your Easel code to be more concise and expressive. Context has been specifically designed so that it is as useful as possible while still being safe and predictable.

Context parameters will automatically find their value from context variables, unless explicitly given a value.

Not every parameter is a context parameter. Context parameters are:

  • Declared inside square brackets [ and ]
  • Or the subject parameter of a function (e.g. fn ship.ActivateWarpDrive, ship is a context parameter)

Not every variable is a context variable. Context variables are:

  • Declared with use: use color=#ff8800
  • Or are function parameters: fn Example(radius) { }
  • Or in the case of this or ui, are implicitly declared within the subblocks of certain functions. It is possible for you to declare other variables to work like this as well, but this should be done sparingly.

let or const variables are not context variables and so can be used freely without worrying about unexpectedly affecting the context.

Context variables are only found in the current scope or in enclosing scopes within the same top-level function. They never leak from one function to another. In general, context variables are only the variables you can see, except for the two cases of this and ui. This is important as it avoids unexpected bugs and makes your code more predictable and easier to debug.

Context is a powerful feature that can greatly reduce the amount of boilerplate code you need to write, enabling you to experiment more easily and create better games, faster.