Friday 28 December 2012

Callbacks and built-in functions as first-class functions

G'day:
This will be my most unplanned blog article to date. I saw a thread on the Railo newsgroup over Xmas, and whilst catching up with my responses just now, decided "oh, I'll be able to come up with an article about that".

The posting on the mailing list was asking whether one can pass a built-in function (BIF) as a callback.

I currently have no idea what I'm about to say about this, so I am as interested in what I continue to type as you are (the conceit is that if you've got this far, you have at least a small amount of interest)...


OK, so what's an example of what's under discussion here? Consider this code:

// generic transformer
array function transformArrayElements(array array, function transformer){
    for (var i=1; i <= arrayLen(array); i++){
        array[i] = transformer(array[i]);
    }
    return array;
}

// transformation
string function strengthen(string string){
    return "<strong>#string#</strong>";
}

// transform the array using the transformation function
strongColours = transformArrayElements(["whero", "karaka", "kakariki"], strengthen);


writeDump(strongColours);

Here we have a simple function - transformArrayElements() - which takes an array, and applies a callback function to each element.

Asides:
  1. If we had delegates in CFML, we could enforce that the callback was a function that takes a string and returns a string, making it more clear that the transformArrayElements() only works with simple array values, but we'll need to wait for that. Anyway, the code does the trick. Raised as E/R 3346444.
  2. This demonstrates that the implementation of arrayEach() in CF10 sux. It's not possible to write this using arrayEach(), because it only receive the element value of the array item, not the array itself, nor the index at which the array element is at. So one cannot update the original array inline, one needs to create a new array. Suck. Raised as bug 3316802.

We then have a simple function, strengthen(), which just slaps <strong> tags around a string.

And last, we call transformArrayElements() with strengthen() as the callback argument, and we get this as a result:

array
1<strong>whero</strong>
2<strong>karaka</strong>
3<strong>kakariki</strong>

No surprises there.

However what if the transformation we wanted to do was to upper-case each element? We might try to do this:

uppercaseColours = transformArrayElements(["whero", "karaka", "kakariki"], ucase);

However this just yields an error:

Variable UCASE is undefined.


The error occurred in C:/webroots/shared/junk/crap.cfm: line 16
14 : 
15 : // transform the array using the transformation function
16 : uppercaseColours = transformArrayElements(["whero", "karaka", "kakariki"], ucase);
17 : 
18 :

(note my descriptive file-naming standard ;-)

The problem here is that CFML BIFs are not "first class functions", meaning - and this is a fairly layman-ish distillation of that Wikipedia article - that they have a "special" status in the language in that they don't count as functions in the sense of a function one might create with a function declaration or function expression, and aren't variables, so can't be used in situations in which the code calls for function variables.

This is not a purely theoretical requirement. Clearly the bod on the Railo forum needed to do this, and I've found myself wanting to do it as well in the past (more than once).  It's easy to work around:

// transformation
string function ucaseWrapper(string string){
    return ucase(string);
}

// transform the array using the transformation function
uppercaseColours = transformArrayElements(["whero", "karaka", "kakariki"], ucaseWrapper);

All I'm doing here is creating a function which wraps a call to the BIF function I want to use, and then calling the wrapper. Note I cannot just do this:

// transformation
string function ucase(string string){
    return ucase(string);
}

Because this just errors with:


The names of user-defined functions cannot be the same as built-in ColdFusion functions.

The name ucase is the name of a built-in ColdFusion function.

I seem to recall some move was afoot a while back to get this restriction removed from CF (Sean, was that your suggestion? I vaguely recall it was?), but it's not happened yet. I think it's been lifted from within CFCs though [checks...] yes it has been. Seems like a bit of a half-arsed job to me.

I suspect that if BIFs were elevated to be first-class functions, this would also solve / remove this restriction too.

This isn't the most exciting improvement to CFML one could make, but I think it's a sensible & slightly handy one. And keeping inline with the zeitgeist of people wanting language improvements, not bells and whistles added to the language.

Thoughts? In the mean time, I've raised an E/R for this: 3434441.

--
Adam


PS: 50min from concept to publish on this article btw. Hope it doesn't show too much ;-)