This section discusses blocks as they were added to TOM by Tesla. The old TOM compiler, tomc, can't handle blocks.
A block is a piece of code that of which the execution is postponed. Instead of the code being executed immediately, an instance of the Block class is created as a placeholder for the piece of code. When the Block is evaluated, through its eval method, the piece of code is actually executed. Here is an example of our favourite program, using a Block.
int main Array arguments { Block b = |{ /* no arguments */ || [[[stdio out] print "hello, world"] nl]; }|; [b eval]; } |
A Block starts with |{ and ends with }|. In between are at least two vertial bars (`|') that precede the actual code that is contained in the Block. The verbose spacing, comment, and assignment to a variable are optional. The same program could look like this:
int main Array arguments { [|{|| [[[stdio out] print "hello, world"] nl]; }| eval]; } |
A Block can have arguments and it can return a value. The arguments are declared like the argument accompanying a method name part: either a single argument name preceded by its type, or an argument tuple preceded by a tuple type. The return type of a Block is not declared: the value returned by a Block is the value of the last expression and its type is deduced by the compiler. In the following example, we employ a Block to return the result of adding two integers:
int main Array arguments { Block adder = |{ (int, int) (i, j) || i + j; }|; int result = [adder eval (24, 42)]; [[[stdio out] print ("24 + 42 = ", result)] nl]; } |
A Block does not need to be evaluated lexically within the enclosing method; it can be evaluated from anywhere. Irrespective of the context in which it is evaluated, a Block can reference the variables in the context where it was created. In the following example, the variable count is incremented twice, and the number printed is 2.
void do Block a_block { [a_block eval]; } int main Array arguments { int count; Block b = |{ || count++; }|; [self do b]; [b eval]; [[[stdio out] print ("count = ", count)] nl]; } |
When a Block references variables from its context, the Block is invalidated when that context exits. The following example shows how such a situation may occur: the Block, created in the one_block_please method, is evaluated after the one_block_please method has exited. As a result, the eval method raises a Condition:
Block one_block_please { int num_invocations; = |{ || ++num_invocations; }|; } int main Array arguments { Block block = [self one_block_please]; // FAIL: the context in which the BLOCK // was created has already exited. int n = [block eval]; [[[stdio out] print ("number of invocations = ", n)] nl]; } |
Apparantly, a Block can not use variables from its context when that context exits before the useful life of the Block has passed. Since self is part of the context, also instance variables can not be used for that purpose. To remedy this, thus allowing a Block to maintain information over invocations irrespective of whether its context has exited, a Block can employ block variables, as shown in the following example, where the Block created has one block variable, num_invocations. Notice how block variables occupy the space between the double vertical bar we used up to now:
Block one_block_please { = |{ /* no arguments */ | int num_invocations; | ++num_invocations; }|; } int main Array arguments { Block block = [self one_block_please]; int n = [block eval]; [[[stdio out] print ("number of invocations = ", n)] nl]; } |
If you want to reference instance variables from a Block after its context has exited, you can do so by declaring a block variable self, as the following example shows. Note how the value that is assigned to the block variable self is the value of the implicit method argument self.
implementation instance TOM { int num_cows; } Block cow_counter { = |{ /* no arguments */ | id self = self; | num_cows++; }|; } int main Array arguments { Block cc = [self cow_counter]; int i; for (i = 0; i < 10; i++) { int cow_i = [cc eval]; [[[stdio out] print ("cow number ", cow_i)] nl]; } [[[stdio out] print (num_cows, " cows")] nl]; } end; |
The eval method of the Block class is declared thus:
dynamic eval dynamic arguments; |
The eval method accepts any argument and returns any type of value. The types of the actual argument and the value returned is deduced by the compiler and administered in its output. At run time, the eval method checks to see that the arguments passed match the arguments expected by the Block, and that the type of value returned by the Block matches the type of value that is expected by the caller. Upon a mismatch, a Condition is raised. As an exception, any type returned by a Block matches an expected return type void.
The overhead in execution time of the eval method is similar to that of the perform with of the All instance. This overhead is somewhat reduced by providing eval methods that are specialized on their types. As an example, this is what the eval method looks like that accepts an int and returns an int:
int (result) eval int a1 { pointer fn = code; if (check_block_selectors && cmd != arguments) if ([self arguments_fail (arguments, cmd)]) return; <c> result = ((tom_int (*) (void *, void *, tom_int)) fn) (self, cmd, a1); </c> } |
In these specialized eval methods, the following variables and methods are used:
an instance variable of the Block that points to the C function which is executed to evaluate the Block;
a class compile option that is usually TRUE; and
a selectors that describes the formal argument and return type of the receiving Block.
a boolean method that returns TRUE if the actual selector passed to an eval method can not fit the Block's formal selector. In fact, when arguments_fail is about to return TRUE, it will raise a Condition.
The Block class comes with some specialized eval methods, including this example int eval int. Additional specialized eval methods, can be added in an extension of the Block class.
TODO: Execution-speed measurements.