Sunday, 23 March 2014

CFML: Built-in functions, UDFs, methods, and function expressions... with the same name

G'day:
I have no idea what I'm playing at. It's 9pm on Saturday night and I am in a downtown Galway ("Galway, Ireland", for you Americans... there's probably a Galway in TX, MN, CA as well ;-) pub, which was nice and quiet this afternoon and good for a quiet drink and some blog research. Now it's a full on Saturday night and everyone else is just partying and I'm still typing. The alternative is sitting here by myself and drinking, looking like... I'm sitting here by myself and drinking (and it'd not be the first time. Even today!). I guess at least I would not be the only person in the place writing a blog article about CFML.

Right, so today's efforts started with the intention of looking at some new methods in Railo: .some(), .every() (there's no docs for these yet - that I can find - but there is a Jira ticket: Add closure functions ArrayMap, ArrayReduce, ArrayEvery, ArraySome functions), and rounding out at look at any other CFML iteration functions I'd not looked at yet. I've looked at some previously from a ColdFusion perspective: "ColdFusion 11: .map() and .reduce()".

But then an interesting discussion came up on the Railo Google Group and the function map() which has been added to the latest Railo BER, which breaks WireBox: "Wirebox breaking on latest Railo patch 4.2.0.006", and this absorbed me for the rest of the afternoon and evening, thinking about it, wittering on on the forum, and testing some code. There's a few concepts discussed on the thread, and it's worth reading in its entirety.

Anyway, I decided to have a look at formalising my understanding of how much overlap is allowed between built-in functions and user-defined functions (be those stand-alone declared in a CFM, declared within a CFC, or defined as a function expression). And the behaviour varies a bit as it turns out.

I got to the point of having the code written yesterday at the pub, but all bar the first paragraph of this article is written the following evening. I pretty much gave up on the pub at that point (and I was actually pretty drunk by then), so waddled off into the night. Well: it was only mid evening actually... about 9pm. OK... it's now Sunday evening and completely pub-free today, and the only cibarial consideration in play at the moment is that I'm a bit hungry. But this does not usually impact my writing, so off we go.

The problem Alex (who started that thread on Google) was having with WireBox is that Railo have introduced a map() function (a generic handler for mapping structs, arrays, etc). This interferes with WireBox's own map() method, which Alex is using. This means in his CFCs when he calls map(), it now calls Railo's map() BIF, instead of WireBox's method of the same name. And so Alex's app goes splat.

If we were talking just a coupla instance of this (and it was solely Alex's own code), he could just qualify the calls to WireBox's map() method thus: variables.map(). That's enough to allow Railo to disambiguate as to which map() function Alex means to call. However there's a lot of code internal to WireBox that suffers the same problem, so that's a PitA. At the same time I muddied the water slightly by positing that the map() function in Railo was a bad decision to implement anyhow, so perhaps the best solution here was to get rid (bear in mind they also have arrayMap() and structMap() and a map() method on arrays and structs as well!).

Anyway, I knocked together some code to have a look at how BIFs and UDFs collide, like I said. Here it is:

//RailoVersion.cfc
component {

    function ucase(s){
        echo("ucase() method was called<br>")
        return s.toUpperCase()
    }

    function viaUnscoped(s){
        return ucase(s)
    }

    function viaScoped(s){
        return variables.ucase(s)
    }

}

<cfscript>
// railoTest.cfm

/*
// will not compile.
function ucase(s){
    writeOutput("ucase() UDF was called<br>");
    return s.toUpperCase()
}
*/

uCase = function(s){
    echo("ucase() function expression was called<br>")
    return s.toUpperCase()
}

o = new RailoVersion()
s = "tahi,rua,toru,wha"

echo("Using method<br>")
echo(o.ucase(s) & "<hr>")

echo("Using unscoped reference within CFC<br>")
echo(o.viaUnscoped(s) & "<hr>")

echo("Using scoped reference within CFC<br>")
echo(o.viaScoped(s) & "<hr>")


echo("Using unscoped ucase() within CFM<br>")
echo(ucase(s) & "<hr>")

echo("Using scoped ucase() within CFM<br>")
echo(variables.ucase(s) & "<hr>")
</cfscript>

Here we have three different versions of a ucase() UDF defined:
  • declared via a function statement as a method in a CFC
  • declared via a function statement as a function in a CFM
  • declared via a function expression as a function in a CFM
The first thing to note here is that the function statement version (in the CFM file) won't even compile. Railo prohibits it:

Railo 4.2.0.006 Error (template)
MessageThe name [ucase] is already used by a built in Function

We'll get back to that. Here's the output of the code above (with that second UDF definition commented-out):

Using method
ucase() method was called
TAHI,RUA,TORU,WHA


Using unscoped reference within CFC
TAHI,RUA,TORU,WHA

Using scoped reference within CFC
ucase() method was called
TAHI,RUA,TORU,WHA

Using unscoped ucase() within CFM
TAHI,RUA,TORU,WHA

Using scoped ucase() within CFM
ucase() function expression was called
TAHI,RUA,TORU,WHA


We see a few interesting things here.
  • Using a method call on an object: no problem, and the UDF is called. This is not surprising.
  • Using an unscoped reference within a CFC calls the BIF, not the UDF. This is what Alex was experiencing with map().
  • Using a scoped reference within the CFC calls the UDF. Calling it this way disambiguates between the BIF and the UDF.
  • Using an unscoped ucase() within a CFM unsurprisingly results in the BIF being called.
  • And finally using a scoped reference to ucase() within a CFM also disambiguates between the two, and the UDF is called.
Also interesting is how I can declare a function with the same name as a BIF via a function statement within a CFC just fine, but I cannot within a CFM. I don't think this is very logical. Especially when one considers that there's actually no problem with doing this at runtime with a function expression. To me this seems like having a rule for the sake of it. There is clearly no harm in defining a function thus; it works fine in a CFC. And there's also no problem with calling it: one just needs to scope it first. I'd like to understand the perceived difference here.

Note: ColdFusion works exactly the same here, barring the syntactical differences between the two CFML dialects.

That's all I have to offer on this topic. I've got about half the code written for the some() / every() article, and I have a few hours to kill at the airport tomorrow, so I'll finish it off and write the article then.

Time for me to investigate the burger joint across the road from the hotel (it's not an unknown to me... it'll be the third meal there this weekend).

--
Adam