Iterator definitions are similar to routine definitions, except that we need to indicate when control should be transferred back to the calling point. In a routine, this transfer of control is indicated by a return statement, which terminates the routine. An iterator, however, can return control in two different ways. It can either
Temporarily yield control to the callig point, ready to continue the next time it is encountered. This yield of control is done by a yield statement
Permanently yield control to the calling loop, terminating the loop in the process. This termination of the enclosing loop is achieved by a quit statemen or by reaching the end of the iteratort.
The yield must return a value (of the appropriate type), if the iterator has a return value.
range!(min, max:INT):INT is i:INT := min; loop until!(i > max); yield i; i := i + 1; end; end; |
This iterator can be used to add up all the numbers in a particular integer range
sum: INT := 0; loop sum := sum + range!(1,10); end; |
When an iterator has yielded as many times as needed, it can either reach the end of it's statement list or explicitly call a quit statement. quit statements are used to terminate loops and may only appear in iterator definitions. No value is returned from an iterator when it quits. No statements may follow a quit statement in a statement list. The following definition of 'range!' is equivalent to the preceeding definition:
range!(min, max:INT):INT is x:INT := min; loop if x > max then quit; end; yield x; x := x + 1; end; end; |
The following figures illustrate the control flow between an interator and its calling loop.
When the iterator is first called, control goes into the iterator and then returns to the outer loop, when the iterator yields in step [7]
After the first yield, control continues in the outer loop until the iterator is encountered again in step [11] and control is again transferred to the iterator, right after the point of the yield, in step [12]
The above sequence will continue until the if statement is true and the quit statement is encountered in the iterator. Control is then transferred to the end of the enclosing loop. The iterator calling context keeps track of the internal state of the iterator from the last yield.
One problem with the above definition of 'range!' is that the arguments to the function will be evaluated each time through the loop. Consider the following loop
sum:INT := 0; max:INT := 5; loop sum := sum + range!(3,max); max := max+2; end; |
This, somewhat silly, example will go into an infinite loop, since the argument 'max' increases each time through the loop.
Iterator argument are hot by default. This means that the arguments will be re-evaluated and passed to the iterator each time through the loop. When the arguments to the iterator are constant, it is not important whether they are re evaluated or not. However, in many cases it is important to ensure that the argument is only evaluated the first time through the loop.
This happens to once-arguments. Arguments which are marked with the mode 'once are only evaluated the first time they are encountered during a loop execution. Thus, the correct definition of the 'range' iterator is:
range!(once min, once max:INT):INT is i:INT := min; loop until!(i > max); yield i i := i + 1; end; end; |
Note that 'once' arguments are only marked at the point of definition, not at the point of call. Thus, invoking the loop will look the same as before
sum:INT := 0; loop sum := sum + range!(3,5); end; |
The 'self' parameter (i.e. the object on which the iterator is being called) is always a once parameter.
i:INT := 5; loop #OUT + i.upto!(11)! + ' '; i := 1; end; -- The above loop prints out 5 6 7 8 9 10 11 |
In the above example, though the value of 'i' changes the second time through the loop, the change is ignored - the first value of 'i' is used.
The following more complex example will sum up some of the elements of the first row although the variable row will contain different rows in consecutive loop iterations.
loop -- Sum up some of the elements of the first row! row := matrix.row!; sum := sum + row.elt!; -- row is only evaluated at the first iteration! end; |
Yield causes assignment to out and inout rguments in the caller i.e. these arguments are assigned each time when the iterator yields..
range!(once min, once max:INT, out val:INT) is i:INT := min; loop until!(i > max); val := i; yield; i := i + 1; end; end; |
Which may be used by:
sum:INT := 0; loop res:INT; range2!(3,5, out res); sum := sum + res; end; #OUT + sum + '\n'; -- Prints out 12 |
Note that no assignment to out and inout arguments takes place when an iterator quits.
The behavior of pre- and post- conditions in iterator definitions is a natural extension of their behavior in routine definitions. The pre clause must be true each time the iterator is called and the post clause must be true each time it yields. The post clause is not evaluated when an iterator quits.
At a more technical level, when an iterator is first called in a loop, the expressions for self and for each once argument are evaluated left to right. Then the expressions for arguments which are not once (in or inout before the call, out or inout after the call) are evaluated left to right. On subsequent calls, only the expressions for arguments which are not once are re-evaluated. self and any once arguments retain their earlier values.
Iterators may only be called within loop statements.
once mode arguments are only marked at the point of definition, not at the point of call, unlike out and inout arguments. out and inout arguments cannot be once arguments as well.
Each textual instance of an iterator in a loop is distinct. The following loop prints out [2,2] [3,3] [4,4] (and not [2,3])
loop a:INT := range!(2,4); b:INT := range!(2,4); #OUT + "[" + a + "," + b + "]"; end; |
Not all iterators reach their end or quit - execution may be terminated because some other iterator in the same loop quit. See the next point.
Iterator instances in a conditional statement are evaluated every time they are encountered. The following loop prints out [2,2] [3,2] [4,3] and then is terminated when the first iterator quits, even though the second iterator is not yet complete
b: INT := 0; loop a:INT := range!(2,4); if a.is_even then b := range!(2,4); end; #OUT + "[" + a + "," + b + "]"; end; |
The expressions for self and for once arguments may not themselves contain iterator calls. (Such iter calls would not be useful anyway, since they would only execute their first iteration.) Thus, the following code is illegal, even though the 'times!' iterator is a perfectly valid iterator on integers.
loop a:INT := range!(3,4).times!; end; |
Iterators may call themselves recursively as routines do. As iterators are normally supposed to yield more than once, one should not forget to define a loop within the iterator to catch all of these results
class BINARY_TREE is attr left,right:SAME; -- subtrees attr data:INT; elt!:INT is if void(self) then quit; end; yield data; loop yield left.elt!; end; -- yield data in the left subtree. loop yield right.elt!; end; end; -- elt! end; -- class BINARY_TREE |
If an iterator in complex expression quits, the surrounding expression might not be fully evaluated.
loop #OUT + "(" + c.elt! + ")\n" end; |
When the iterator elt! terminates the surrounding loop, an opening bracket has already been printed. The expression producing the matching closing bracket will not be evaluated, hence the algorithm will always print a bogus closing bracket in the end. The standard solution looks as follows:
loop #OUT + ( "(" + c.elt! + ")\n" ); end; |
The extra paratheses force the whole line to be evaluated first. As this evaluation will be aborted by the quit of the iterator the printing evaluation will not happen for the last iterator call.
Iterator names always end with an exclamation mark '!'.
Yield is not permitted within a protect statement (see the Chapter on Exceptions)
Iterators enjoy the same access options as routines. Just as with routine definitions, iterator definintions may be marked private.
Iterator overloading and conformance rules are the same as those for routines.
An iter argument may have only one mode. Thus, it is neither possible nor meaningful to have 'once inout' or 'once out' arguments.