Monday, 14 October 2013

CFCamp: Loop labels in Railo

G'day:
Here's another one that I already knew about, but hadn't looked at it before. Gert brought it up in his keynote today, so I thought I'd have a look. Railo has added labels to <cfloop> and <cfbreak> / <cfcontinue>. But the implementation is either incomplete or non-intuitve (at least to me).

Here's an example, demonstrating how Railo can label a loop, and then reference that label in a <cfbreak> or <cfcontinue>:

<cfset innerThreshold = 6>
<cfset outerThreshold = 11>

<cfoutput>
<cfloop index="x" from="1" to="5" label="outer">
    Outer: x:#x#<br>
    <cfloop index="y" from="1" to="5" label="inner">
        Inner: y:#y#<br>
        <cfset sum = x+y>
        <cfset product = x*y>
        sum:#sum#; product:#product#<br>
        <cfif product GT outerThreshold>
            #product# > #outerThreshold#: Breaking out of outer<br>
            <cfbreak "outer">
        </cfif>
        <cfif sum GT innerThreshold>
            #sum# > #innerThreshold#: Continuing out of inner<br>
            <cfcontinue "inner">
        </cfif>
        Bottom of inner loop<br>
    </cfloop>
    Bottom of outer loop<hr>    
</cfloop>
</cfoutput>

This outputs:

Outer: x:1
Inner: y:1
sum:2; product:1
Bottom of inner loop
Inner: y:2
sum:3; product:2
Bottom of inner loop
Inner: y:3
sum:4; product:3
Bottom of inner loop
Inner: y:4
sum:5; product:4
Bottom of inner loop
Inner: y:5
sum:6; product:5
Bottom of inner loop
Bottom of outer loop

Outer: x:2
Inner: y:1
sum:3; product:2
Bottom of inner loop
Inner: y:2
sum:4; product:4
Bottom of inner loop
Inner: y:3
sum:5; product:6
Bottom of inner loop
Inner: y:4
sum:6; product:8
Bottom of inner loop
Inner: y:5
sum:7; product:10
7 > 6: Continuing out of inner
Bottom of outer loop

Outer: x:3
Inner: y:1
sum:4; product:3
Bottom of inner loop
Inner: y:2
sum:5; product:6
Bottom of inner loop
Inner: y:3
sum:6; product:9
Bottom of inner loop
Inner: y:4
sum:7; product:12
12 > 11: Breaking out of outer

This is ludicrously contrived, but the gist is that the loops have labels, and <cfbreak> and <cfcontinue> can break/continue a specific loop, not simply the "nearest" one to them. Handy!

This however doesn't work with Railo's generic script treatment of <cfloop>:

loopThreshold = 3;

loop index="x" from="1" to="5" label="loopLabel" {
    if (x >= loopThreshold){
        break "loopLabel";
    }
}

This errors with:

Railo 4.1.0.004 Error (template)
MessageWrong Context, tag cfbreak must be inside a cfloop or cfwhile tag with the label [loopLabel]
StacktraceThe Error Occurred in
C:\webroots\railo-express-4.1.0.004-jre-win32\webapps\www\shared\git\blogExamples\railo\loopLabelsScript1.cfm: line 6 
4: loop index="x" from="1" to="5" label="loopLabel" {
5: if (x >= loopThreshold){
6: break "loopLabel";
7: }
8: }

It looks like the CFScript parser hasn't been updated to expect this. To test the syntax, I just converted this back to tags:

<cfset loopThreshold = 6>

<cfloop index="x" from="1" to="5" label="loopLabel">
    <cfif x GE loopThreshold>
        <cfbreak "loopLabel">
    </cfif>
</cfloop>

This is the same code, except with the <cf and > put back on, and it works (ie: it compiles. I know the code itself doesn't really demonstrate breaking out of labelled loops in a sensible way). So I think my attempted CFScript syntax is correct, but it dun't work.

I also tried with a proper for() loop, and that just errored too. I really had to guess at the syntax for that, though:

loopThreshold = 3;

for (x=1; x <= 5; x++; "loopLabel") {
    if (x >= loopThreshold){
        break "loopLabel";
    }
}

So that was a bad guess, as this is what happened:

Railo 4.1.0.004 Error (template)
Messageinvalid syntax in for statement, for statement must end with a [)]
StacktraceThe Error Occurred in
C:\webroots\railo-express-4.1.0.004-jre-win32\webapps\www\shared\git\blogExamples\railo\loopLabelsScript2.cfm: line 4 
2: loopThreshold = 6;
3: 
4: for (x=1; x <= 5; x++; "loopLabel") {
5: if (x >= loopThreshold){
6: break "loopLabel";

Also... no mention of loop labels in the docs for <cfloop>, either.

Update:
I collared Mark Drew in the bar last night, and he gave me the correct syntax for labelled loops in CFScript:

loopThreshold = 3;

loopLabel: for(x=1; x <= 5; x++){    
    writeOutput("Top of loop with #x#<br>");
    if (x >= loopThreshold){
        break loopLabel;
    }
    writeOutput("Bottom of loop<br>");
}
writeOutput("After loop<br>");

This makes sense, in hindsight: decoupling the label from the looping construct. I means labels can be used by other constructs too.<cfgoto>, anyone? ;-)

I did test with a conditional loop, and that worked as one would expect:

<cfset count = 0>
<cfset threshold = 5>
<cfset outerPanicAt = 10>
<cfset outerPanicCount = 0>
<cfset innerPanicAt = 10>
<cfset innerPanicCount = 0>

<cfloop condition="outerPanicCount LE outerPanicAt" label="outer">
    <cfset outerPanicCount++>
    <cfoutput>outerPanicCount: #outerPanicCount#<br></cfoutput>
    <cfloop condition="innerPanicCount LE innerPanicAt">
        <cfset innerPanicCount++>
        <cfoutput>innerPanicCount: #innerPanicCount#<br></cfoutput>
        <cfset count++>
        <cfoutput>count: #count#<br></cfoutput>
        <cfif count GE threshold>
            Threshold met<br>
            <cfbreak "outer">
        </cfif>
        Bottom of inner loop<br>
    </cfloop>
    Bottom of outer loop<br>
</cfloop>
After loops<br>

This outputs:

outerPanicCount: 1
innerPanicCount: 1
count: 1
Bottom of inner loop
innerPanicCount: 2
count: 2
Bottom of inner loop
innerPanicCount: 3
count: 3
Bottom of inner loop
innerPanicCount: 4
count: 4
Bottom of inner loop
innerPanicCount: 5
count: 5
Threshold met
After loops


Spot on.

So not a bad feature, but maybe not as complete as I've come to expect from the Railo guys. I've definitely needed this in the past, but I mostly use CFScript so I'd like to see support extended out to script code too.

--
Adam