This article is about looping constructs like FOR and
WHILE that are known from real programming languages, but not
explicitely implemented in HSC.
If you have programmed in LISP (or SCHEME or Amiga
InstallerScript—if that's how you call the language) before, it may not
come as a big surprise that a macro language like HSC can actually do loops. At
first it's a weird thought for those who haven't though: there's not even a
GOTO, only seemingly linear expansion of, possibly nesting,
macros. It's the nesting of macros that is the key, because when you call a
macro from within itself, you get a recursive procedure (I don't call
it a function because it can not return a value to its caller in HSC). HSC will
happily compile something like
<$macro ENDLESS><ENDLESS></$macro>
...and eventually abort, crash or dump core as soon as you use this, because it gets into an infinite loop and eats all memory it can get before giving up. That's not how recursion is supposed to be. We need a termination condition, upon which the macro stops calling itself. This means we have to pass a parameter from one "level" to the other that tells the macro when to stop. The easiest variant is a counter that terminates the recursion when it is zero:
<$macro RECURSE COUNT:num/R>
<$if COND=(COUNT > "0")>
<RECURSE COUNT=(COUNT - "1")>
</$if>
</$macro>
If you call it as <RECURSE COUNT=5>, it will count down
to zero and then exit, producing nothing but a few blank lines, unless you
compiled with COMPACT enabled. There should be another parameter
to feed the macro some content:
<$macro RECURSE COUNT:num/R CONTENT:string/R>
<$if COND=(COUNT > "0")>
<RECURSE COUNT=(COUNT - "1") CONTENT=(CONTENT)>
<(CONTENT)>
</$if>
</$macro>
Call it as: <RECURSE COUNT=5 CONTENT="Hello,
world!<BR>">:
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
There's our loop! Five instances of the content you only typed once. Next, we'll look at counting in intervals, with steps and contents from containers. Maybe you can figure out some of that already.
Certainly, this iterator is not the best we can do. It lacks the ability to count in different directions, different steps, and it can only count to zero. All of this is fairly easy to fix, so let's start with the first two shortcomings as they can be treated with just one extra parameter. Obviously, the line that reads
<RECURSE COUNT=(COUNT - "1") CONTENT=(CONTENT)>
has to be changed not to subtract one all the time but to
subtract—or rather, as count-up loops are the more frequent use case, add—a
constant we pass in as a parameter. That makes the new RECURSE
macro look like this:
<$macro RECURSE COUNT:num/R STEP:num/R CONTENT:string/R>
<$if COND=(COUNT > "0")>
<RECURSE COUNT=(COUNT & STEP) STEP=(STEP) CONTENT=(CONTENT)>
<(CONTENT)>
</$if>
</$macro>
Now that the count-up case is the normal one, this macro dearly needs an
upper limit, or it will count forever unless a negative STEP is
specified. Adding this is just as easy:
<$macro RECURSE COUNT:num/R TO:num/R STEP:num/R CONTENT:string/R>
<$if COND=(COUNT < TO)>
<RECURSE COUNT=(COUNT & STEP) TO=(TO) STEP=(STEP) CONTENT=(CONTENT)>
<(CONTENT)>
</$if>
</$macro>
That's everything you could ask from an iterator macro already! It can count
in different directions from a certain start value, you can specify the
counting step and the termination value. But it's not very nice to have
to specify all this, and to pass the content as a string, which can become
quite long.
The issue of container macros has been treated already in the article on
HSC and scripting. Read it if you want
to avoid confusion while writing your own iterator wrappers, though the
following should be obvious. In addition to the features described for the
scripting macros, it makes use uf HSC's default parameters for macros,
so you don't have to specify START or STEP if the
defaults are OK:
<$macro FOR /CLOSE START:num=1 TO:num/R STEP:num=1> <RECURSE COUNT=(START) TO=(TO) STEP=(STEP) CONTENT=(HSC.Content)> </$macro>
Try it:
<FOR START=4 TO=8> Howdy!<BR> </FOR>Howdy!
Now you can wrap arbitrary pieces of text, with HTML formatting, HSC macros or anything you like, in a container macro and have them repeated a certain number of times. Nice as it is, all this counting in steps and forwards and backwards doesn't help much if you can't access the counter in the "body" of your loop. But you can already! Look:
<FOR START=4 TO=12 STEP=2> Counting: <(COUNT)><BR> </FOR>Counting: 10
Oops! Well, nice counting, steps and all, but it's the wrong direction. Have
a look at the RECURSE macro again:
<$macro RECURSE COUNT:num/R TO:num/R STEP:num/R CONTENT:string/R>
<$if COND=(COUNT < TO)>
<RECURSE COUNT=(COUNT & STEP) TO=(TO) STEP=(STEP) CONTENT=(CONTENT)>
<(CONTENT)>
</$if>
</$macro>
Imagine how the expansion takes place when you call it like above. You'll get a result like below if you expand it on all levels and substitute the arguments:
<$if COND=(4 < 12)>
<$if COND=(6 < 12)>
<$if COND=(8 < 12)>
<$if COND=(10 < 12)>
<$if COND=(12 < 12)>
</$if>
Counting: <(COUNT)><BR>
</$if>
Counting: <(COUNT)><BR>
</$if>
Counting: <(COUNT)><BR>
</$if>
Counting: <(COUNT)><BR>
</$if>
That is, the arguments expanded first come from the inner levels of recursion
where COUNT is highest. To fix this, it's sufficient to swap the
expansion of <(CONTENT)> and the recursion part:
<$macro RECURSE COUNT:num/R TO:num/R STEP:num/R CONTENT:string/R>
<$if COND=(COUNT < TO)>
<(CONTENT)>
<RECURSE COUNT=(COUNT & STEP) TO=(TO) STEP=(STEP) CONTENT=(CONTENT)>
</$if>
</$macro>
That's it! Now the counting goes the right direction as specified in the
FOR. The expansion of <(CONTENT)> before the
next <$if>-construction gives you another advantage: you can fiddle
with COUNT or other values used in RECURSE from your
loop's "body", thus manipulating the loop much like you could
do1 in languages
like C, e.g. set TO to -1 in a count-up loop to leave it
prematurely.
In the scripting article I mentioned that the multiplication table I used as an example for PERL scripting can just as well be done in HSC alone, albeit with a couple of macros that are much harder to fully understand. It's now time to rewrite the table using the constructs presented so far. We'll start with the static stuff and two nested loops:
<$macro multtab SIZE:num=5>
<TABLE border="2">
<TR>
<FOR START=1 TO=(SIZE)>
<FOR START=1 TO=(SIZE)>
</FOR>
</FOR>
</TABLE>
</$macro>
But wait—the counter is called "COUNT" in both
loops, so how can the inner loop access the outer's counter? It has to be
renamed!
<$macro multtab SIZE:num=5>
<$define y:num>
<TABLE border="2">
<TR>
<FOR START=1 TO=(SIZE)>
<$let y=(COUNT)>
<FOR START=1 TO=(SIZE)>
</FOR>
</FOR>
</TABLE>
</$macro>
This defines the temporary variable "y" that is set to the
outer loops's counter value just before entering the inner loop. OK, now for
the table headings. The script
creates a colored border around the actual table, the first line of which is
done in a loop of its own:
<$macro multtab SIZE:num=5>
<$define y:num>
<TABLE border="2">
<TR><TH></TH>
<FOR START=1 TO=(SIZE)>
<TH bgcolor="#9090f0"><(COUNT)></TH>
</FOR></TR>
<FOR START=1 TO=(SIZE)>
<$let y=(COUNT)>
<FOR START=1 TO=(SIZE)>
</FOR>
</FOR>
</TABLE>
</$macro>
The first line is done, now for the actual multiplications. Every line gets a
header cell with colored background and the value of y first, then
a row of cells with results. And that's it for the table macro! See the result
on the right...
| <multtab SIZE=10>2 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<$macro multtab SIZE:num=5>
<$define y:num>
<TABLE border="2">
<TR><TH></TH>
<FOR START=1 TO=(SIZE)>
<TH bgcolor="#9090f0"><(COUNT)></TH>
</FOR></TR>
<FOR START=1 TO=(SIZE)>
<$let y=(COUNT)>
<TR><TH bgcolor="#9090f0"><(y)></TH>
<FOR START=1 TO=(SIZE)>
<TD><(y * COUNT)></TD>
</FOR>
</TR>
</FOR>
</TABLE>
</$macro>
|
|
Easy enough if you see it developing, and if you don't have to think about what's happening "under the hood" at the same time. You see, the concept of stepwise abstraction you use in "real" programming can be useful even for a fairly simple macro language. The recursive expansion facility is too weird to use directly, unless you're a die-hard LITHP fan, but properly wrapped you can use it to write something that at least resembles an interpreted programming language and that is no more complicated or hard to write than Perl, Python or—shudder—BASIC.
Being able to nest loops is definitely a Good Thing—but the method of renaming the counter we had had to use in the previous example is a bit awkward. It should be possible to automate this as well.
What has to be done is to pass in some extra HSC code to
ITERATE that does this, and then let FOR generate the
necessary instructions from a simple variable name. To do the last step first:
let's extend FOR with an extra parameter called
VAR. How to define a variable whose name isn't known in advance?
Dynamic code generation does the trick:
1 <$macro FOR /CLOSE START:num=1 TO:num/R STEP:num=1 VAR:string>
2 <$if COND=(set VAR)>
3 <* create loop-local variable VAR *>
4 <("<$define " + VAR + ":num>")>
5 <* call ITERATE with COUNT-renaming code for current VAR *>
6 <ITERATE COUNT=(START) TO=(TO) STEP=(STEP)
7 CONTENT=("<$let " + VAR + "=(COUNT)>" + HSC.Content)>
8 <$else>
9 <ITERATE COUNT=(START) TO=(TO) STEP=(STEP) CONTENT=(HSC.Content)>
10 </$if>
11 </$macro>
Line 4 shows the simplest way of using dynamically generated code there is:
it contains HSC's "insert expression" construct
<(...)> with a string expression that builds
a $define-tag using the variable name passed in as
VAR. When this is expanded, a local variable with the requested
name is defined for the rest of the macro. Because it is local, it doesn't hurt
to have the same name in the macro's scope already, as the local version will
temporally supersede this one.
The only thing we have to ensure now is to have the extra code expanded
before the actual loop's contents. It could be passed in to
ITERATE via an extra parameter, which is how I did it in the first
versions of this macro, but there's an even easier way as you can see in lines
6 and 7: as HSC.Content is just a string, it can be concatenated
with other code. In this case, all we have to do is to slap an extra
$let-tag in front of the content-string, so it gets expanded each
time the content is used.
There you are! Now you can nest loops as easily as
this:
<FOR VAR=i TO=10>
<FOR VAR=j TO=5>
Nested loops: <(i + ',' + j)><BR>
</FOR>
</FOR>
If you don't want to use the VAR attribute, the innermost
counter ist still available as COUNT of course.
whilePerhaps this one should have come before the FOR loop, because
the latter can easily be derived from it, but then again it's a little more
complicated because of its dynamic HSC code.
Anyway—parameter-wise, while is very simple. As the only
attribute, it takes a condition that must evaluate to TRUE (i.e.
non-zero) for the loop to continue running, and of course it should be a
container macro that repeats its contents:
<$macro WHILE /CLOSE COND:string/R> </$macro>
The attribute COND is called the same as $if's
attribute, but with an important and unfortunately unavoidable difference: it
is a string, not an expression, although it will be evaluated as one
internally. So while you write <$if COND=(a < 10)>
for a simple conditional, the corresponding loop syntax is
<while COND="a < 10">3.
Having read the development of the FOR tag, you can guess the
next step—creating an iterator "function". Only that in this case it has
to be able to accept an expression instead of a range of numbers, and iterate
until this evaluates to FALSE:
<$macro WHILE-ITER COND:string/R CONTENT:string/R>
<("<$if COND=(" + COND + ")>")>
<(CONTENT)>
<WHILE-ITER COND=(COND) CONTENT=(CONTENT)>
</$if>
<TAG>/$macro</TAG>
How to evaluate the condition we got as a string attribute? Same old
problem, same old solution :) As you can see above, all it takes is to
construct an $if-tag on the fly and expand it at the same time.
As the condition doesn't have to be modified before the recursion, that's all
there is to the iterator already.
And yes, of course FOR could be based on this. The code to do
the counting would have to be added to the CONTENT attribute of
course, either by string concatenation before the first iteration, or with an
extra attribute for WHILE-ITER similar to FOR's
CODE attribute.
COUNT < TO", so
SIZE=10 only makes a
9x9 table, just like the usual C version
for(COUNT=1; COUNT<TO; ++COUNT) would.
<$if>
like this:<$macro IF /CLOSE COND:string/R><$if
COND=(COND)><(HSC.Content)></$if></$macro>.Last change: 21-Feb-2006, 06:43