Using Labeled Loops In ColdFusion
The other day, I was watching a tutorial on the Svelte JavaScript framework and I saw that they used the $:
notation to denote reactive values. I believe that this is just an old JavaScript feature for labeling parts of the code (which Svelte is then overloading). I've never used that JavaScript feature myself; but, it did remind me that some of this labeling concept is also available in ColdFusion as well, specifically for loops. I've never used this feature in ColdFusion either; so, I thought it might be worth a quick exploration.
I couldn't find much on how this feature actually works outside of this article on the ColdFusion blog. The idea is that you can add a label to either a for
or a while
loop; and then, consume that label in either a break
or a continue
statement. Essentially, you can tell ColdFusion which loop you are referencing in your break
/ continue
statements.
Which, is really only meaningful if you have nested loops. Otherwise, the break
and continue
statements simply reference the one contextual loop that you are in.
Part of me feels that labeling loops falls under the "too clever" umbrella of programming approaches; and, is likely to make the code harder to read and maintain. But, if encapsulated behind some meaningful abstraction, it could be OK to use from time to time.
And, to be honest, I had a hard time thinking of a real-world scenario in which I might want to have nested loops with label-based control flow. What I eventually came up with was a fuzzy-matching algorithm for text search. Given a target value and a search value, we want to determine if ever letter within the search value can be located - in order - within the target value, even if not within a contiguous string of characters.
For this, I'm going to have two nested loops:
- Looping over the search characters.
- Looping over the target characters.
As I compare one character to another, I'm going to have opportunities to both break
and continue
from the inner loop to the outer loop. This is hard to explain in words, so let's take a look at the ColdFusion code (which works in both Adobe ColdFusion and Lucee CFML) - note that the outer loop is labeled searchLoop:
and the inner loop is labeled targetLoop:
:
<cfscript>
// Fuzzy matches.
writeOutput( isFuzzyMatch( "horse", "s" ) & "<br />" );
writeOutput( isFuzzyMatch( "horse", "hs" ) & "<br />" );
writeOutput( isFuzzyMatch( "horse", "horse" ) & "<br />" );
// No matches.
writeOutput( isFuzzyMatch( "horse", "horses" ) & "<br />" );
writeOutput( isFuzzyMatch( "horse", "test" ) & "<br />" );
writeOutput( isFuzzyMatch( "horse", "" ) & "<br />" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I determine if the given target text is a fuzzy-match for the given search text.
*/
public boolean function isFuzzyMatch(
required string targetValue,
required string searchValue
) {
var searchChars = searchValue.listToArray( "" );
var targetChars = targetValue.listToArray( "" );
var matchFound = false;
searchLoop:
while ( searchChars.len() ) {
var s = searchChars.shift();
targetLoop:
while ( targetChars.len() ) {
var t = targetChars.shift();
// We found a matching CHARACTER in the target string.
if ( s == t ) {
if ( ! searchChars.len() ) {
matchFound = true;
// If we've run out of search-characters to consume, it means that
// the entirety of the search keyword was located (in parts)
// within the target text. In other words, we HAVE a fuzzy-match.
// Yay! At this pointer, there is nothing left to search and we
// can break out of BOTH the INNER and OUTER loops.
break searchLoop;
}
// If we still have more search characters to consume, move onto the
// NEXT ITERATION of the OUTER loop, and the next search character.
continue searchLoop;
}
}
// If we've fully consumed the target characters, there's no sense in
// continuing to consume the search characters - we will not find a match.
break;
}
return( matchFound );
}
</cfscript>
NOTE: In this code, there is no need for me to label the inner loop,
targetLoop:
- I'm just doing it to demonstrate that it can be done.
As you can see, whenever I match a search character against a target character, I perform a control-flow operation that references the outer loop, searchLoop:
. If I've consumed all of the search characters, I break
out of outer loop (essentially ending the algorithm); and, if there are more search characters to consume, I continue
onto to the next iteration of the outer loop. Both of these operations are performed from within the inner loop.
If we run this ColdFusion code, we get the expected outcome:
true <!-- isFuzzyMatch( "horse", "s" ) -->
true <!-- isFuzzyMatch( "horse", "hs" ) -->
true <!-- isFuzzyMatch( "horse", "horse" ) -->
false <!-- isFuzzyMatch( "horse", "horses" ) -->
false <!-- isFuzzyMatch( "horse", "test" ) -->
false <!-- isFuzzyMatch( "horse", "" ) -->
This demo uses script-based loops; but, apparently this is also available in tag-based loops as well.
I'm not sure if the value-add of this technique outweighs the potential cognitive cost. But, it's probably worth having this loop labeling concept tucked away in the back of my mind in case I ever need it.
Epilogue: Tag-Based Labeled Loops
Above, I mentioned that labeled loops were also available in tag-based CFML. As such, I thought it might be worth translating the previous script-based syntax into tags just to see what that would look like, especially since the Adobe ColdFusion documentation for CFBreak
and CFContinue
mention nothing about this. Thankfully, the Lucee CFML documentation does mention that the label should be provided as a string literal.
<cfoutput>
<!--- Fuzzy matches. --->
#isFuzzyMatch( "horse", "s" )#<br />
#isFuzzyMatch( "horse", "hs" )#<br />
#isFuzzyMatch( "horse", "horse" )#<br />
<!--- No matches. --->
#isFuzzyMatch( "horse", "horses" )#<br />
#isFuzzyMatch( "horse", "test" )#<br />
#isFuzzyMatch( "horse", "" )#<br />
</cfoutput>
<cffunction name="isFuzzyMatch" returntype="boolean" output="false">
<cfargument name="targetValue" type="string" required="true" />
<cfargument name="searchValue" type="string" required="true" />
<cfset var searchChars = searchValue.listToArray( "" ) />
<cfset var targetChars = targetValue.listToArray( "" ) />
<cfset var matchFound = false />
<cfloop label="searchLoop" condition="searchChars.len()">
<cfset var s = searchChars.shift() />
<cfloop label="targetLoop" condition="targetChars.len()">
<cfset var t = targetChars.shift() />
<!--- We found a matching CHARACTER in the target string. --->
<cfif ( s == t )>
<cfif ! searchChars.len()>
<cfset matchFound = true />
<!---
If we've run out of search-characters to consume, it means that
the entirety of the search keyword was located (in parts)
within the target text. In other words, we HAVE a fuzzy-match.
Yay! At this pointer, there is nothing left to search and we
can break out of BOTH the INNER and OUTER loops.
--->
<cfbreak searchLoop />
</cfif>
<!---
If we still have more search characters to consume, move onto the
NEXT ITERATION of the OUTER loop, and the next search character.
--->
<cfcontinue searchLoop />
</cfif>
</cfloop>
<!---
If we've fully consumed the target characters, there's no sense in
continuing to consume the search characters - we will not find a match.
--->
<cfbreak />
</cfloop>
<cfreturn matchFound />
</cffunction>
Notice that the CFLoop
now has a label
attribute; and, that the searchLoop
reference is boolean attribute on both the CFContinue
and CFBreak
tags. This tag-based code, of course, gives us the same output as the script-based code above ... though, with significantly more syntax.
Want to use code from this post? Check out the license.
Reader Comments
Very interesting. I've been coding in CF since late 1990's, and I had no idea the labels existed. One real-world example using them would be scraping documents for specific types of data. (Scraping is one of my specialties, and trust me, there's a lot of conditional logic going on in scraping.) But hving said that, yeah.... using labels badly could lead to spaghetti-y code like these old-school BASIC programs using GOTO's too much. :-)
@Angeli,
To be fair, I think labeled-loops was only added in recent versions of ColdFusion -- the one article that I found on this references ACF 2021 as being the version in question. But, I can't find a "history" section on the docs page, like they used to have.
Re: scraping, I remember waaaaay back in the day, Yahoo had some service where you could turn any webpage into a SQL data-source. I think they called it YQL (Yahoo Query Language). I did a couple of little things where I used that to scrape some pages. But, I think they got rid of that service pretty quickly 😆
Great, but in the example you don't need any of this. On line 37 instead of setting matchFound = true, just return true. No reason to keep going any further. You don't even need the matchFound variable. Replace line 53 with a normal break, since breaking from inner loop will do same as continuing the outer loop. Line 67, just return false since if it was ever true it would have returned already.
@Emre,
This is a great point-out. And, in fact, in my original code, I have the early
return(true)
to short-circuit the evaluation. I put thematchFound
variable in there in order to explore the labeled-looping a bit more.That said, your second
break
will break the algorithm. Essentially, you stop searching after the first character match. Imagine that my target text ishorse
and you're fuzzy searching forhoe
. In your looph
would match, so you break out of the inner loop. But then, you immediately break out of the outer loop as well, leading to afalse
(which should have beentrue
). That's why I have thecontinue searchLoop
.I think my previous post didn't account for the last break in the outer while loop. Instead of using a break when it reaches the end, it needs a condition in the outer while, but below is the same code without the labeled out-of-sequence-breaking loops.
@Emre,
Ah, adding a check for both the search and target characters length in the outer-loop is clever. Yes, I believe this would get you want you wanted.
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →