Monday 23 June 2014

arrayMap(): a reverse CFML history

G'day:
This is a bit of a nostalgia trip. But until those nostalgia trips that are all sepia-coloured and star Jimmy Stewart and harken back to better times: this should make us thankful we are where we are.

Last week I decided to clear out a few of the UDFs in the CFLib backlog. I wrote an article about one of the ones I processed in my last article - "Feedback on UDF submitted to CFLib" - and I'm gonna continue on with the next one I looked at.

This one is a facsimile of arrayMap(): back porting the functionality of ColdFusion 11's new function to work on older versions of CF. As written, it would have only worked back as far as CF8, but it was a reasonable effort. On CFLib I don't give a toss about versions of CF prior to CF9 (and even then, only until December when it is end-of-lifed; after that I'll only be targeting a minimum of CF10), so the version I release is CF9 compatible. And is as follows:

array function arrayMap(required array array, required any f){
    if (!isCustomFunction(f)){
        throw(type="InvalidArgumentException", message="The 'f' argument must be a function");
    }

    var result = [];
    var arrLen = arrayLen(array);
    for (var i=1; i <= arrLen; i++){
        arrayAppend(result, f(array[i], i, array));
    }
    return result;
}

(I'm actually glad I revisited this, as I spotted a bug with it when I did: I had some CF10-only code in it).

That works fine. It lacks the elegance of ColdFusion 11's implementation as it cannot use an inline function expression (which is kinda the whole "thing" about these iteration functions), but it does the mapping OK.

This got me thinking (and mildly investigating) how CFML has changed over the years, and what code one would need to write to effect this functionality using progressively older versions of CFML. To start with, let's look at the bleeding edge state of affairs (the bleeding edge in this context currently being Railo 4.2, not ColdFusion 11).

Railo 4.2

// railo4_2.cfm
mappedArray = ["tahi","rua","toru","wha"].map(function(required string string){
    return ucase(string
})
dump(mappedArray)


ColdFusion 11

And I'll contrast this with the subtly different ColdFusion 11 version:

// cf11.cfm
originalArray = ["tahi","rua","toru","wha"];

mappedArray = originalArray.map(function(required string string){
    return ucase(string);
});
writedump(mappedArray);

There are three things to note here:

  1. Railo can call methods directly on literal values, whereas ColdFusion needs an intermediary variable;
  2. Railo doesn't require semi-colons (except where vagaries of their CFML implementation means the code would be ambiguous without them);
  3. Railo calls it dump(), not writeDump().
The latter two are unrelated to the map() functionality itself, I know, but it indicates a progression onwards from ColdFusion 11's CFML implementation.

ColdFusion 10


// cf10.cfm
array function arrayMap(required array array, required function callback){
    var mappedArray = [];
    var arrLen        = arrayLen(array);

    for (var i=1; i <= arrLen; i++){
        arrayAppend(mappedArray, callback(array[i], i, array));
    }
    return mappedArray;
}

mappedArray = arrayMap(["tahi","rua","toru","wha"], function(required string string){
    return ucase(string);
});
writedump(mappedArray);

The chief difference here is that ColdFusion 10 doesn't yet implement arrayMap(), so we need to roll our own. Meaning it cannot be called as a member function, as per CF11 and Railo.

ColdFusion 9


// cf9.cfm
array function arrayMap(required array array, required any callback){
    var mappedArray = [];
    var arrLen        = arrayLen(array);

    if (!isCustomFunction(callback)){
        throw(type="InvalidArgumentException", message="The 'callback' argument must be a function");
    }

    for (var i=1; i <= arrLen; i++){
        arrayAppend(mappedArray, callback(array[i], i, array));
    }
    return mappedArray;
}

string function toUpper(required string string){
    return ucase(string);
}

mappedArray = arrayMap(["tahi","rua","toru","wha"], toUpper);
writedump(mappedArray);

ColdFusion 9 lacks two features of ColdFusion 10 here:

  1. no function return type, so we need to do our own type-checking (or just let the subsequent code error out, but that's a bit sloppy);
  2. and no function expressions, so we need to create a UDF for toUpper().

ColdFusion 8


ColdFusion 8's CFML implementation is beginning to make our code need to be a bit grim:

<cfscript>
// cf8.cfm
function arrayMap(array, callback){
    var mappedArray = [];
    var arrLen        = arrayLen(array);
    var i            = 0;

    if (!structKeyExists(arguments, "array")){
        throw(type="MissingArgumentException", message="The 'array' argument is required");
    }
    if (!isArray(array)){
        throw(type="InvalidArgumentException", message="The 'array' argument must be a function");
    }
    if (!structKeyExists(arguments, "callback")){
        throw(type="MissingArgumentException", message="The 'callback' argument is required");
    }
    if (!isCustomFunction(callback)){
        throw(type="InvalidArgumentException", message="The 'callback' argument must be a function");
    }

    for (i=1; i <= arrLen; i++){
        arrayAppend(mappedArray, callback(array[i], i, array));
    }
    return mappedArray;
}

function toUpper(string){
    return ucase(string);
}

originalArray = ["tahi","rua","toru","wha"];
mappedArray = arrayMap(originalArray, toUpper);
</cfscript>
<cfdump var="#mappedArray#">


  1. No type-checking or requiredness-checking on UDF definitions, meaning we have to roll our own. Yes I know I could do this in tags, but... bleah.
  2. All var statements must be at the top of the function, whether we need the variable or not.
  3. Array-literal notation is supported, but only when assigning to a variable. I cannot be used implicitly when passing a value to a function call.
  4. writeDump() doesn't exist!
Note: I wrote a CF8 version at home and tested it, but forgot to commit it. So I've rewritten it from memory here (in the office), but haven't tested this code as I have no CF8 available here to test on. I'll re-check it when I get home.

CFMX7


Shudder. CFScript in CFMX7 is not fit for purpose, so I am diving back into tags now:

<!--- cfmx7.cfm --->
<cffunction name="arrayMap" returntype="array" output="false">
    <cfargument name="array" type="array" required="true">    
    <cfargument name="callback" type="any" required="true">    

    <cfset var mappedArray    = arrayNew(1)>
    <cfset var arrLen        = arrayLen(array)>
    <cfset var i            = 0>

    <cfif not isCustomFunction(callback)>
        <cfthrow type="InvalidArgumentException" message="The 'callback' argument must be a function">
    </cfif>

    <cfloop index="i" from="1" to="#arrayLen(array)#">
        <cfset arrayAppend(mappedArray, callback(array[i], i, array))>
    </cfloop>
    <cfreturn mappedArray>
</cffunction>

<cfscript>
originalArray        = arrayNew(1);
originalArray[1]    = "tahi";
originalArray[2]    = "rua";
originalArray[3]    = "toru";
originalArray[4]    = "wha";

function toUpper(string){
    return ucase(string);
}

mappedArray = arrayMap(originalArray, toUpper);
</cfscript>
<cfdump var="#mappedArray#">


  1. As I mentioned: CFScript is not up to spec for serious usage in CFMX7, so I've reverted to tags here.
  2. Not least of all because I cannot even throw an exception in CFScript.
  3. There is no array-literal notation in CFMX7.

CFMX6.x

I do no have CFMX6.0 or 6.1 available to test on, so did not do an example for this version. From memory one cannot have var statements in CFScript on 6.x, because of the rule that "all var statements must come at the top of the function", and this even counts the opening <cfscript> tag.

Other than that... I don't think there would be any differences to the CFMX7 version.

ColdFusion 5

I'm back to CFScript here, because CF5 did not support functions in tags!

<cfscript>
// cf5.cfm
function arrayMap(array, callback){
    var mappedArray = arrayNew(1);
    var i            = 0;
    var arrLen        = arrayLen(array);

    for (i=1; i LTE arrLen; i=i+1){
        arrayAppend(mappedArray, callback(array[i], i, array));
    }
    return mappedArray;
}

originalArray        = arrayNew(1);
originalArray[1]    = "tahi";
originalArray[2]    = "rua";
originalArray[3]    = "toru";
originalArray[4]    = "wha";

function toUpper(string){
    return ucase(string);
}

mappedArray = arrayMap(originalArray, toUpper);
</cfscript>
<cfdump var="#mappedArray#">

  1. As I said: no tag-based functions in CF5.
  2. We can also do no type validation any more. We just need to let the code error if it gets the wrong types.
  3. We're back to using old-school operators.

But - credit where it's due - we can still implement the functionality.

ColdFusion 4.5


Blimey. No functions. This one was tricky, but I think I have a custom-tag-based approximation of the required functionality. Note: I do not have CF4.5 installed so cannot test this. But I think it's sound.

<cfscript>
//cf4_5.cfm
originalArray        = arrayNew(1);
originalArray[1]    = "tahi";
originalArray[2]    = "rua";
originalArray[3]    = "toru";
originalArray[4]    = "wha";
</cfscript>

<cf_arraymap array="#originalArray#" callbackArgs="callbackArgs" returnVariable="mappedArray">
    <cf_toupper value="#callbackArgs.value#">
</cf_arraymap>

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

<!--- arrayMap.cfm --->
<cfif NOT thistag.HasEndTag>
    <cfthrow type="MissingClosingTagException" message="The 'arraymap' tag must have a closing tag">
</cfif>
<cfparam name="attributes.array" type="array">

<cfparam name="attributes.callbackArgs" type="variableName">
<cfparam name="attributes.returnVariable">

<cfset mappedArray = arrayNew(1)>
<cfif THISTAG.ExecutionMode EQ "Start">
    <cfset myArray = arrayNew(1)>
    <cfset index = 1>
    <cfset "caller.#attributes.callbackArgs#" =structNew()>
    <cfset "caller.#attributes.callbackArgs#.value" = attributes.array[index]>
    <cfset "caller.#attributes.callbackArgs#.index" = index>
    <cfset "caller.#attributes.callbackArgs#.array" = attributes.array>
<cfelse>
    <cfset index = index + 1>
    <cfif index gt arrayLen(attributes.array)>
        <cfset tempArray = arrayNew(1)>
        <cfloop index="i" from="1" to = "#arrayLen(thistag.callbackResults)#">
            <cfset arrayAppend(tempArray, thistag.callbackResults[i].value)>
        </cfloop>
        <cfset "caller.#attributes.returnVariable#" = tempArray>
        <cfexit method="exittag">
    <cfelse>
        <cfset "caller.#attributes.callbackArgs#" =structNew()>
        <cfset "caller.#attributes.callbackArgs#.value" = attributes.array[index]>
        <cfset "caller.#attributes.callbackArgs#.index" = index>
        <cfset "caller.#attributes.callbackArgs#.array" = attributes.array>
        <cfexit method="loop">
    </cfif>
</cfif>

<!--- toupper.cfm --->
<cfif thistag.HasEndTag>
    <cfthrow type="IllegalClosingTag" message="The 'toupper' tag must not have a closing tag">
</cfif>
<cfparam name="attributes.value">

<cfif THISTAG.ExecutionMode EQ "Start">
    <cfset attributes.value = ucase(attributes.value)>
    <cfassociate basetag="cf_arraymap" datacollection="callbackResults">
    <cfexit method="exittag">
</cfif>

Key points:

  1. It's all custom tags;
  2. there's no such thing as <cfdump>;
  3. what a lot of bloody horsing around.
The rest speaks for itself (and this is not a tutorial on custom tags, so I won't explain it all).

Conclusion

I'm not going to go any further back... I just wanted to take it back to the beginning of CFML's function support, and then one step further to demonstrate what was necessary before functions existed.

It's pleasing to see:
  • it's still possible to implement said functionality even without functions!
  • How far we've come.
And that's it. arrayMap() through the ages.

--
Adam