Got a question that the wiki doesn't answer? Ask on the forum (preferred), or join us on IRC.

BeastNode

CommandHelper/Staged/Closures

From EngineHub.org Wiki
Jump to: navigation, search


A closure is a block of code that is treated as code, not as a result of code. Closures are actually used quite frequently in your code already, and you just aren't aware, however, with the closure architecture, you yourself can harness the power of a closure, and in fact they are required to be used in many of the newer, more powerful functions.

Explanation

So, what is a closure? You can read the wikipedia article if you wish, but it's fairly complex. So, instead, we'll discuss something that you probably are already familiar with; the if() function.

The if function internally implements closures, transparently to you. Let's take a look at some code:

1   if(true){
2        msg('Hello World!');
3   } else {
4        msg('You should not see me!');
5   }


If you notice, this code will only message you "Hello World", it will not message the other code. However this will message you twice:

1   array(msg('One message'), msg('Two message'));


So, why is that? That's because the code in the if function is lazily evaluated. That is to say, it's not evaluated until it is determined that it is needed. Other functions do this too, some for optimization reasons, others, because it's just the nature of the function. For instance, for() does as well. In most cases, the "special" functions, (functions that have a language construct dedicated to them in other languages) implement closures. Some functions, such as and also are lazily evaluated, for performance reasons. Consider the case: and(false, true, true, true, ....lots of trues). If we evaluate from left to right (which we do), after we determine that the first argument evaluates to false, we know for a fact that the and will return false, regardless of what the rest of the arguments return. Same with or and true values: or(true, false, false, false).

However, in the case of the array(), this has to be hastily evaluated, that is, we need to know how ALL the arguments are going to resolve, before we send them to the array function itself. So if we break it down, with if, we are sending code to the function, and with array we are sending values to the function. When we are sending code to a function, as an executable set of instructions, we say we have created a closure.

When we think about the proc function, we then realize that it's much like a closure. When we define the procedure, nothing is run at that time. And in fact, internally, it does create a type of closure. Procs are slightly special though, because they do not save environment information (at least variable scope). Let's observe the more complicated case with an if statement:

1   @a = 'variable';
2   if(true){
3        msg(@a);
4   }


This works, and messages "variable" like we would expect. However, the following does not work the same:

1   @a = 'variable';
2   proc _doit(){
3        msg(@a);
4   }
5   _doit();


This will send us a blank message, because @a is not in scope inside of the proc. This makes it not a true closure, by the technical definition, but it's pretty close. The difference is that the environment is not stored along with the code. A mscript closure uses this concept, but also adds an extra feature, the ability to pass in extra environment information upon actual execution of the code.

Using closures

Using the closure() and execute() functions allows you to harness the power of closures yourself, however you wish.

closure()

The signature of the closure function looks quite similar to proc.

1   closure([@vars...,]){
2      // Some code
3   }


The vars passed in must be ivars, but much like procs, you may assign default values to them.

1   closure(@a = 'a value'@b){
2        msg(@a @b)
3   }


Also, note that the code inside the closure has access to the special variable @arguments, which is an array containing all the arguments passed to the closure.

Values may also be returned from the closure, using return():

1   closure(){
2      return(5);
3   }


execute()

Execute is used as follows:

1   execute([values...,] <closure>);


The values may be any value, but the closure must be a closure created with the closure function. A small, self contained example then:

1   execute(1, 2, 3, closure(@a@b@c){
2      msg(@a . @b . @c);
3   });


Any values returned from the closure are returned by execute.

1   @five = execute(closure(){
2      return(5);
3   });
4   msg(@five); // 5


Combined Usage

The closure function actually returns a special data type, a closure. This data type has all the same qualities of other data types, that is, it can be stored in a variable, passed around to procedures, returned, imported/exported, persisted. It's very much like an anonymous procedure, and in fact has nearly the same signature as the proc function, minus the name. That's because you essentially give it a name when you store it somewhere, for instance in a variable.

1   @a = closure(){
2      msg('This is some code that will run later');
3   };
4   // Now we have defined this closure in the variable @a
5   execute(@a); // Here, we actually execute the closure, we now get msg'd!


We also have access to variables that are passed in, if we named them:

1   @a = closure(@one@two){
2      msg(@one @two);
3   };
4   execute('one''two'@a); // msg's "one two"


Also, we can implement vararg functionality with the special @arguments variable:

1   @a = closure(){
2      msg('You passed in ' . array_size(@arguments) . ' arguments');
3   };
4   execute(1, 2, 3, 4, 5, @a); //msg's "You passed in 5 arguments"


If you define more parameters than are passed in, they take the default values that are assigned.

1   @a = closure(@one@two = 'two'){
2      msg(@one . ' ' . @two);
3   };
4   execute('one'@a); // msg's "one two"


Scope

So, you may ask yourself, what's the difference between this and a proc, other than the name is missing? A huge difference. With a proc, all variables go out of scope, other than what you pass in. With closures, everything that was in scope remains in scope. The only thing that may get overridden is if you named a variable that is passed in, and in addition, the meta scope values, such as the current player also remain in scope.


Recall the example above:

1   @a = 'variable';
2   proc _doit(){
3        msg(@a);
4   }
5   _doit();


If we rewrite this with closures, we can see the difference:

1   @a = 'variable';
2   @doit = closure(){
3        msg(@a);
4   };
5   execute(@doit);


In the first example with procs, we get an empty message. With the second example, we get msg'd "variable", because @a remained in scope when we executed the closure.

Now, it is important to note that the scope is frozen at closure bind time, NOT at execution time, so the following will happen:

1   @value = 'Hello World!';
2   @closure = closure(){
3      msg(@value);
4   };
5   // Let's change the value stored in @value
6   @value = 'Goodbye World!';
7   // Now we execute the closure, which echoes what it thinks @value is
8   execute(@closure); // msg's "Hello World", not "Goodbye World"


This is because when we execute the closure, the values have already been bound, and can no longer be changed, as the closure sees it. This doesn't mean that something can't pass in extra values though, that's where the extra arguments come from. Let's modify the previous example some.

1   @value = 'Hello World!';
2   @closure = closure(@value){
3      msg(@value);
4   };
5   // Let's change @value again
6   @value = 'Goodbye World!';
7   // Now pass @value back in when we execute the closure
8   execute(@value@closure); // This time it does echo "Goodbye World"


As you can see, we are able to change the environment at execution time if we put a little more thought into it.

Another important point to make here is to remind you that arrays are passed by reference, so the following is possible:

1   @a = closure(@array){
2      @array[0] = 'Hello World';
3   };
4   @value = array();
5   execute(@value@a);
6   msg(@value); // msg's "{Hello World}", because the array can be edited from inside the closure


Assigning a variable inside of a closure will not affect the external variable table however, so this code will function as follows:

1   @a = closure(@array){
2      @array = null;
3   };
4   @value = array();
5   execute(@value@a);
6   msg(@value); // msg's array; it's still an array!


This is because arrays are passed by reference, not by value, and it's simply the reference that is getting copied into the closure's @array variable. So, if inside the closure, we change the values pointed at by the reference, those changes are visible from outside the closure. However, in the second example, we're changing the reference to the array, and simply putting something else inside of it. This doesn't affect the outside code having a reference to the array though, it still is pointing to the same empty array it created earlier.

iclosures

An iclosure is the same thing as a closure, except the variable scope is different. The variable scope works like a procedure; no variables from the parent scope are retained. An iclosure with no arguments passed to it, then, has no variables in scope (other than @arguments). This is useful as an optimization technique, when you have a closure in a large program, it has to copy all the references of all the currently in scope variables. If all the values you need are going to be passed into the closure, there's no point in doing all that copying. Instead, you can just use an iclosure, thus possibly reducing your memory footprint. An iclosure is identical to closures in all other ways, and they can be used in any case that a closure is required.

Serialization of closures

Closures can be serialized too! They get output as a basic string, however, so it's important to note the advantages/disadvantages of this. When they get serialized, they completely loose their scope. They become a standalone string. If you output the string, it may not look precisely like it did when you put it in either, in fact it will have sconcats added, and be minified, however, the guarantee is that it is functionally equivalent. However, do note that if you import/export the closure, the scope will remain, it is only if you move the closure outside of CH that it will lose it's scope (for instance, with store_value/get_value). If the closure is to-string'd, you will have to use eval() on it, instead of execute, and if previously it had accepted values, you'll have to manually assign them yourself. They are not meant for out of memory storage, and so they do not support that very well, however, it is important to note what their behavior would be if you attempt to serialize them.

1   msg(closure(){
2      msg('This is a' . ' closure');
3   });
4   // msg's "msg('This is a closure')", which is functionally equivalent to the above code


Common usage

The most common usage that you will find is likely when you use a built in function that requires use of a closure. You may also use them in a library project, where you are wanting to create a callback of some sort, or as a way to abstract up a loop, for instance.



Navigation menu