So far we have explored the usage of functions in GML 2.3; however, this is not the full picture. In the examples we've looked at so far, there is actually a decent amount of work that the compiler is hiding from us. In order to fully understand the intricacies of functions and how they deal with scoping we must intimately understand what methods are and how they work under the hood.
Let's first revisit the definition of a function in GML: code to be executed. Recall that functions are actually resources, with a resource index stored in a real
.
A method is a function bound to a struct (or instance) via the variable self
.
So what does this mean exactly? On the surface this may seem like an insignificant distinction. This distinction is critically important when we start considering the context in which functions are called and execute from.
Let's begin by exploring how the compiler hides methods from us when dealing with global functions:
/// @script LogUtility
function trace(message) {
show_debug_message("[" + string(id) + "] " + message);
}
/// oExample : Create
trace("Instance created!");
/// @script spawn_examples
instance_create_layer(0, 0, "Instances", oExample);
instance_create_layer(0, 0, "Instances", oExample);
// => [1000000] Instance created!
// => [1000001] Instance created!
Notice how in the global function trace
is contextually printing the correct id
depending on what instance of the object oExample
is calling it. In order for this to work, there's actually some additional magic the compiler is doing for us behind the scenes.
Let's look at the code it's hiding from us:
/// @script LogUtility
globalvar trace;
trace = method(undefined, function (message) {
show_debug_message("[" + string(id) + "] " + message);
});
/// oExample : Create
trace("Instance created!");
/// @script spawn_examples
instance_create_layer(0, 0, "Instances", oExample);
instance_create_layer(0, 0, "Instances", oExample);
// => [1000000] Instance created!
// => [1000001] Instance created!
The only changes we can see have to do with the function declaration. Notice how the function name becomes an implicit globalvar
, and is assigned to the return value of a call to method
passed undefined
and an Anonymous Function!
To understand this let's take a look at the built-in function method
. This function takes a struct or instance id for its first parameter and an inline function for its second. When undefined
is passed, it tells the GML Runtime to bind whoever is the caller to self
rather than retain a strong reference to a single instance/struct. This is why global functions always bind to the scope of whatever instance or struct has called them. When functions are declared inside of objects, they are assigned as an instance variable with self
bound to the id
of that instance.
We can explore a similar example but with the trace
function being declared inside an object:
/// oExample : Create
function trace(message) {
show_debug_message("[" + string(id) + "] " + message);
}
trace("Instance created!");
/// @script spawn_examples
var foo = instance_create_layer(0, 0, "Instances", oExample);
instance_create_layer(0, 0, "Instances", oExample);
foo.trace("Called from outside!");
// => [1000000] Instance created!
// => [1000001] Instance created!
// => [1000000] Called from outside!
The behavior demonstrated above should now make sense given what we know about what the compiler is doing for us behind the scenes. Let's take a closer look at what exactly is being compiled though:
/// oExample : Create
trace = method(id, function (message) {
show_debug_message("[" + string(id) + "] " + message);
});
trace("Instance created!");
/// @script spawn_examples
var foo = instance_create_layer(0, 0, "Instances", oExample);
instance_create_layer(0, 0, "Instances", oExample);
foo.trace("Called from outside!");
// => [1000000] Instance created!
// => [1000001] Instance created!
// => [1000000] Called from outside!
Again, we can see that the only real change to the code is that we are really assigning to the instance variable trace
the result of a call to method
passed id
and the Inline Function equivelent of our Named Function.
An important distinction to understand is that when we use the () operator, we are calling a method not a function. In the same way we work with instances not an object during runtime. In fact, this is a very good way to conceptualize the differences between a method and function. Keeping this in mind will make understanding function scopes significantly easier. Simply ask yourself "What is self
bound to? An id, struct or undefined?"
TODO Introduce structs and constructors before this section
So far we have only explored how functions get translated to methods behind the scenes in the context of scripts as global functions, or in the context of objects as instance functions. Next let us take a look at methods in structs.
function Inventory() constructor {
items = ds_list_create();
add = function (itemId) {
ds_list_add(items, itemId);
};
printContents = function () {
for (var i = 0, isize = ds_list_size(items); i < isize; ++i)
show_debug_message(items[| i]);
};
}
var foo = new Inventory();
foo.add("Sword");
foo.add("Shield");
var bar = new Inventory();
bar.add("Potion");
foo.printContents();
// => Sword Shield
bar.printContents();
// => Potion
Notice how the instance variable items
is accessible from inside of the functions add
and printContents
. Additionally, we can observe that the foo
and bar
instances of Inventory
contain different items in their respective lists.
Let's explore what the compiler is doing for us behind the scenes:
globalvar Inventory;
Inventory = method(undefined, function () {
items = ds_list_create();
add = method(id, function (itemId) {
ds_list_add(items, itemId);
});
printContents = method(id, function () {
for (var i = 0, isize = ds_list_size(items); i < isize; ++i)
show_debug_message(items[| i]);
});
}
var foo = (method({}, Inventory))();
foo.add("Sword");
foo.add("Shield");
var bar = (method({}, Inventory))();
bar.add("Potion");
foo.printContents();
// => Sword Shield
bar.printContents();
// => Potion
This is a very rough approximation of what the bytecode is doing, this may not compile/run as expected. id
does not actually function this way in the runtime, for instance.
Ok, there's a lot more going on here than our previous examples. First of all, we can see that our constructor function is reduced to merely being a global function like what we're used to seeing. However, that constructor
keyword is very important, notice how it changes what the inline functions are binding to. Instead of undefined
they're bound to id
, which during runtime will refer to either foo
or bar
depending. The next thing we notice is what happened to our new Inventory()
. It actually implicitly allocated an empty object and invoked a method bound to it and the function Inventory
. This makes a lot of sense when you consider the behavior of new
.
Finally we have methods in functions. This is an extremely useful pattern, but there's a few caveats to consider. First let's explore the simpliest use case.
function foo() {
return function () {
return 1234;
};
}
var bar = foo();
bar();
// => 1234
We'll forego the deconstruction of the compiler's magic for the time being. We can see that calling foo
returns an inline function which we then store in bar
. Calling bar
then returns 1234. This is interesting, but not particularly useful.
Let's explore a more useful example:
function make_item_use_callback(itemId) {
return function () {
chat_add_message("You used a " + itemId);
};
}
var swordUse = make_item_use_callback("Sword");
swordUse(); // ERROR! Variable itemId not set before reading it.
Ok so we get an error trying to do this. This actually shouldn't surprise you, as it follows the same scoping rules that we observed looking at how method
is being used by the compiler. Let's take a look at what the compiler is doing here so we can better understand how to fix this error.
globalvar make_item_use_callback;
make_item_use_callback = method(undefined, function (itemId) {
return method(undefined, function () {
chat_add_message("You used a " + itemId);
});
});
var swordUse = make_item_use_callback("Sword");
swordUse(); // ERROR! Variable itemId not set before reading it.
Even looking at what's going on behind the scenes the problem is still subtle. Notice how the method returned from make_item_use_callback
is not bound to an instance, and itemId
is a local variable, so it's only accessible in the scope of make_item_use_callback
, not the returned inline function.
We can fix this by manually binding the inline function to a proxy object:
function make_item_use_callback(itemId) {
return method({
itemId: itemId
}, function () {
chat_add_message("You used a " + itemId);
}};
}
var swordUse = make_item_use_callback("Sword");
swordUse();
// => You used a Sword
The important distinction to understand here is that itemId
inside the body of the inline function is actually referencing self.itemId
.
TODO Explore limitations of proxy objects