Sunday 15 July 2012

<cfparam>: the expression in the default attribute value is always evaluated

Yeah, OK, I'm writing a blog entry about <cfparam>, which I think ought probably be considered perhaps the most boring topic ever.  Sorry about that, but: it's a) Sunday; b) before I've had any coffee.

Word of warning: make sure you've had your coffee before wading through this ;-)

This is another thing that cropped up in a conversation I was having the other day: even if the variable being paramed is already set, the expression in the default attribute value is always still evaluated.  Before anyone panics, I don't mean "evaluated" in the sense of the oft-maligned evaluate() function, I just mean the expression is processed / executed / whatever.

A bunch of people probably already know this, but a bunch of people probably don't.  The target audience here is the latter.  For my part: I am certainly one of those that this didn't occur to me until it bit me on the bum one day, and then I slapped my forehead and went "well: duh!", but I had to have the bum-biting happening first.  So I'm trying to avoid derrière dentition for other people here.

Right: so what am I blathering about?

Here's a common use-case we've all encountered using a <cfparam> call:

<cfparam name="form.myTextField" default="">
<input name="myTextField" value="#form.myTextField#" />

We param form.myTextField to an empty string so that we don't need to worry about whether the form is an initial submission, or a re-display of an earlier submission: the form field is populated with its most recent value.  This is seemingly an abbreviated way of doing this:

<cfif not structKeyExists(form, "myTextField")>
    <cfset form.myTextField = "">
<input name="myTextField" value="#form.myTextField#" />

And indeed in the given situation, I'd use the <cfparam> version myself: one line of code vs three lines of code to achieve the same ends seems like a good thing to me.

So what's this problem I've decided to bang on about today?  I stress that in the above code, there is no problem.  However what about this code:

<cfset = "Initial value">

<cfparam name="" default="#getFoo()#">
<cfdump var="#request#">

<cffunction name="getFoo">
    getFoo() was called<br />
    <cfset sleep(3000)><!---emulate some processing that takes a while --->
    <cfreturn "Default value">

This, predictably, outputs this:

 getFoo() was called
fooInitial value

Brief digression:

I need to complain to Adobe about not doing their housework properly, in leaving that "cfdumpinited" dangling out into the calling code.  Sloppy work there.
EDIT:  I've raised a bug for this with Adobe.
End of digression

In this example, the value we want to use as the default for is the result of a function call which performs a lot of complicated logic and takes some time (disguised as a single call to sleep() for the sake of expediency... ;-)

So?  One might expect that this doesn't matter... if it takes a long time to calculate the value for, so be it.  It doesn't matter here because already has a value, so the <cfparam> won't do anything.

Well it won't do anything to the value of, but it will still actually run.  It's not like the ColdFusion server will go "oh, I can just ignore that <cfparam> line of code, because I've already got a value for".  No.  Because it's the inner-workings of <cfparam> that makes the decision as to whether to set, and for the inner workings to be called, then all the <cfparam> tag's attributes need to be resolved (evaluated / executed / "run"), and then passed to the code that actually does the work.

So whilst the end result of the <cfparam> call is pretty much nothing to the calling code ( stays as-is), setFoo() still gets run so as to pass-in its results to the internal <cfparam> code.  And in this example that causes an unnecessary three second delay to our request.

If there's just a bit of a delay, we can probably live with that sometimes (and, no, we should never live with a three second delay, that is not "a bit of a delay", that's unacceptable), but what if setFoo() actually has side-effects?  In the example, it does have a trivial and very contrived side-effect: it outputs a message to the screen.  However it could log something, or write to a DB, or [some other thing] that we don't want happening if the results of the call aren't actually being used.

I have a rule of thumb for this sort of thing:
  • if the default value for the <cfparam> tag is a simple value (hard-coded string or number, etc), then it's fine to go ahead and use <cfparam>;
  • otherwise I use the structKeyExists() test I mentioned above.
As with all rules of thumb, I treat it as "advice" not a hard-and-fast rule, because rules of thumb always have exceptions.

One thing to take away from this is that CFML tags work kinda like how custom tags work.  If you had a custom tag call like this:

<cf_myTag foo="#getFoo()#">

You'd probably not be surprised that  getFoo() gets called before the value for foo is passed into the code of myTag.cfm... but it's the same for CFML tags (and for the same reason).  Bear in mind that a CFML tag is just a wrapper for Java method calls under-the-hood, and so the CFML expressions need to be executed before they're passed to the Java methods.

And conveniently the coffee shop will be open now (no, I do not have the apparatus in the house to make coffee.  Yeah, I know...), so that's it from me for the day.

WAKE UP everyone!


Relevant docs: