Sunday 28 February 2016

ColdFusion 2016: improvement to cfloop (of all things)

G'day:
I don't think I'll be able to spin this out very far, but it's another small feature of ColdFusion 2016 which is good, and seems to work properly. Well I say "good", and I call it a "feature", but it's really more "mundane" and just a case of finally implementing something almost correctly, instead of the initial attempt which was ballsed-up.

Back in ColdFusion 8, Adobe added the ability to <cfloop> to loop over an array directly, rather than need to use the <cfloop> tag's equivalent of an index for loop. So prior to ColdFusion 8, we needed to do this:


<cfset rainbow = ["Whero","Karaka","Kowhai","Kakariki","Kikorangi","Poropango","Papura"]>

<cfloop index="i" from="1" to="#arrayLen(rainbow)#">
    <cfoutput>#rainbow[i]#<br></cfoutput>
</cfloop>

This outputs the predictable:

Whero
Karaka
Kowhai
Kakariki
Kikorangi
Poropango
Papura


That's fairly innocuous, but iterating over an array in a view is sufficiently common a task that it made sense to fine-tune <cfloop> to accommodate it more semantically. So in ColdFusion 8 we got this instead:


<cfloop array="#rainbow#" index="colour">
    <cfoutput>#colour#<br></cfoutput>
</cfloop>

Well done, Adobe. This would have been so easy to get right, but they managed to balls it up. Here's a quick lesson in arrays:


rainbow = [];
rainbow[1] = "Whero";
rainbow[2] = "Karaka";
rainbow[3] = "Kowhai";
rainbow[4] = "Kakariki";
rainbow[5] = "Kikorangi";
rainbow[6] = "Poropango";
rainbow[7] = "Papura";

Arrays have two relevant components in this exercise: the element (which contains the value), and the index, which indicates the relative position of the element in the array. This isn't just terminology I'm making up... it's fundamental to how arrays work. And it's simple, and it's a CS 101 sort of thing (maybe CS 102?).

I missed this at the time it went into CFML as I did not bother with the CF8 pre-release, so it was out the door before I was aware of it. How everyone who did show up to the PR managed to not notice is beyond me. I imagine people simply didn't care, or were too busy playing with Flash Forms or whatever was the shiny gewgaw in that release. I mean it's trivial, but it's yet another example of Adobe rushing headlong on their mission of making CFML shit, instead of improving it.

Anyway, that was all a digressive framing exercise. The ColdFusion community finally managed to convince Adobe to set things straight (see 3321646, and 3341256), after only 3.5 years and two ColdFusion releases. We finally now have this:

<cfloop array="#rainbow#" index="i" item="colour">
    <cfoutput>#i#: #colour#<br></cfoutput>
</cfloop>

Notice how if I specify both an index and an item, the array loop actually exposes both the index (and it actually is the index), and the item for the element. We can't blame Adobe for using the term item here rather than element as - for once - they took advice to follow what Railo had already done. And item ain't so bad. And is actually correct-ish, which index never was.

If one specifies only the index in the loop, then it still receives the element value. For the sake of backwards compat. This was supposed to be deprecated, but it doesn't seem to be. I will follow this up (commented added to 3321646, to get it reopened).

And if one specifies only item, then it works as one would predict:

<cfloop array="#rainbow#" item="colour">
    <cfoutput>#colour#<br></cfoutput>
</cfloop>

Adobe have also extended this to list looping and file looping too:

<cfset week = "Rāhina,Rātū,Rāapa,Rāpare,Rāmere,Rāhoroi,Rātapu">

<cfoutput>
<cfloop list="#week#" index="day">
    #day#<br>
</cfloop>
<hr>

<cfloop list="#week#" index="i" item="day">
    #i#: #day#<br>
</cfloop>
<hr>

<cfloop list="#week#" item="day">
    #day#<br>
</cfloop>
<hr>
</cfoutput>

Rāhina
Rātū
Rāapa
Rāpare
Rāmere
Rāhoroi
Rātapu


1: Rāhina
2: Rātū
3: Rāapa
4: Rāpare
5: Rāmere
6: Rāhoroi
7: Rātapu


Rāhina
Rātū
Rāapa
Rāpare
Rāmere
Rāhoroi
Rātapu



<cfsetting enablecfoutputonly="true">

<cfoutput><pre></cfoutput>
<cfloop file="#getCurrentTemplatePath()#" index="line">
    <cfoutput>#encodeForHtml(line)#<br></cfoutput>
</cfloop>
<cfoutput></pre></cfoutput>
<cfoutput><hr></cfoutput>

<cfoutput><pre></cfoutput>
<cfloop file="#getCurrentTemplatePath()#" index="i" item="line">
    <cfoutput>#i#:    #encodeForHtml(line)#<br></cfoutput>
</cfloop>
<cfoutput></pre></cfoutput>
<cfoutput><hr></cfoutput>

<cfoutput><pre></cfoutput>
<cfloop file="#getCurrentTemplatePath()#" item="line">
    <cfoutput>#encodeForHtml(line)#<br></cfoutput>
</cfloop>
<cfoutput></pre></cfoutput>
<cfoutput><hr></cfoutput>

(I'll spare you the output: you get the idea).

Enigmatically, they did not extend this to structs. I'd expect this to work:

<cfset numbers = [
    one = "tahi",
    two = "rua",
    three = "toru",
    four = "wha",
    five = "rima",
    six = "ono",
    seven = "whitu",
    eight = "ware",
    nine = "iwa",
    ten = "tekau"
]>

<cfoutput>
<cfloop index="index" item="item" collection="#numbers#">
    #index# #item#<br>
</cfloop>
</cfoutput>

The analogue here is that when one specifies both index and item, then those receive the key and the value. But... no. We just get a compile error:

The following information is meant for the website developer for debugging purposes.
Error Occurred While Processing Request

Attribute validation error for tag CFLOOP.

It has an invalid attribute combination: collection,index,item. Possible combinations are:





  • Required attributes: 'file,index'. Optional attributes: 'charset,from,to'.
  • Required attributes: 'file,index,item'. Optional attributes: 'charset,from,to'.
  • Required attributes: 'file,item'. Optional attributes: 'charset,from,to'.
  • Required attributes: 'index,item,list'. Optional attributes: 'delimiters'.
  • Required attributes: 'index,list'. Optional attributes: 'delimiters'.
  • Required attributes: 'item,list'. Optional attributes: 'delimiters'.
  • Required attributes: 'group'. Optional attributes: 'endrow,groupcasesensitive,startrow'.
  • Required attributes: 'group,query'. Optional attributes: 'endrow,groupcasesensitive,startrow'.
  • Required attributes: 'query'. Optional attributes: 'endrow,startrow'.
  • Required attributes: None. Optional attributes: None.
  • Required attributes: 'array,index'. Optional attributes: None.
  • Required attributes: 'array,index,item'. Optional attributes: None.
  • Required attributes: 'array,item'. Optional attributes: None. <l...

  • Heaven forbid Adobe do a thorough job here.

    Also note the error messages is actually truncated half-way through a tag, and before the various attribute combos even get around to mentioning structs. Sigh.

    Oh, and one cannot use this syntax to iterate over a query either.



    This all got me thinking, actually. Why do we have different constructs for looping over an array, query, list or struct? Or file for that matter. How come it's not just this:

    <cfset myCollection = getCollection()>
    
    <cfoutput>
    <cfloop collection="#myCollection#" key="key" value="value">
        #key#: #value#<br>
    </cfloop>
    </cfoutput>
    

    Where I'm using getCollection() to be purposely value. That could return anything iterable: an array, a list, a file object, a struct, a query. Anything that implements some notional "Iterable" interface really. But <cfloop> could have one uniform syntax for iterating over said collection. Would that not be better? (Raised as 4122711).

    But anyway. As far as they have implemented this feature: it works. It perhaps shoulda been extended to structs and queries too though, yeah? And perhaps as a unified syntax. Oh well.

    Righto.

    --
    Adam