Thursday 6 December 2012

Constants for CFML

G'day:

Updated 2024-08-30

Reworded to avoid my misuse of the word "singleton" which I was using to mean "an object that is created once and reused", which is not a singleton. A singleton is about implementation, not usage. This was pointed out to me by John Whish of the CFML community, during a conversation we were having about misuse of that term.

This one is just an idea I had for ColdFusion 11 (or Railo... just "for CFML" really). Well it's not an idea that's particular to me, nor is it a new one. Or an earth-shattering one. Or one that will sell any licences in and of itself. It would be handy though.

CFML has no notion of named constants, EG:

const PI = 3; // near enough ;-)

Whilst one can created variables, not all values are supposed to be variable. For example π doesn't change: it's not a variable. It doesn't vary.


In one's code, one can simply create a variable and then endeavour not to change it (which is one of the usual arguments against this feature); or one could simply adopt a special naming policy to reflect the intent:

myFavouriteSong = "Gaskrankinstation";
MY_NAME = "Adam"; // I'm happy with that, so it's unlikely to change

But this kinda misses the point: it's nice to be "declarative" about these things, rather than implementing it by convention.

Setting things like π (and, yes, I know CFML has a pi() function... the thing is, it shouldn't be a function, it should just be a constant as per how java.lang.Math implements PI) and my name are pretty nebulous examples. Where do I use the notion of constants on a day to day basis?

At work, I pass the time meddling with an accommodation-booking website.  It's implemented in a dozen-or-so different languages, and a coupla dozen currencies. These all have integer IDs as well as a string abbrev., eg: language 123 is "EN" (English) and currency 234 is "NZD" (New Zealand Dollar).  Sometimes we have logic-exceptions depending on the language or currency, so we need to identify the current language/currency in code. Legacy code might say this:

if (currentCurrency == 123){
    // no need to convert it
}else{
    // do a conversion
}

Which is all well and good whilst the "special" currency is 123, but what if that changes? We've got stackloads of instances of "123" to change. Fortunately "123" is easy to spot & globally search and replace... but what if it was "1"? That would be a nightmare.

What would be good to do is to have a constant exposed in our currency service, ie:

component {

    this.DEFAULT_CURRENCY = 123;
    
    // etc 

}

So we can implement our code that needs to check against that to:

if (currentCurrency == currency.DEFAULT_CURRENCY){
    // no need to convert it
}else{
    // do a conversion
}

And if we happen to move our operation from the UK to NZ, we simply change Currency.cfc to reflect that.

There's two flaws with this. Firstly, we're just using the naming-convention approach to suggest DEFAULT_CURRENCY is a constant. There's nothing to stop someone changing it, or it accidentally being changed. Or someone not understanding that it's supposed to be a constant.  Also - and this is more theoretical than practical - this.DEFAULT_CURRENCY is exposed to calling code - good - but as it can be changed by the calling code, we cannot reliably use it within the code of the CFC, as there's an amount of uncertainty as to what its value is.  So we need to do this:

component {

    variables.DEFAULT_CURRENCY    = 123;
    this.DEFAULT_CURRENCY        = variables.DEFAULT_CURRENCY;
    
    // etc 

}

So we use variables.DEFAULT_CURRENCY within the CFC as we can be certain it hasn't been meddled with, and we expose a copy of it to the calling code, and the CFC doesn't need to care about the integrity of its value.  Although even then, as this CFC is instantiated as an application-scoped shared object, if some code somewhere accidentally changes this value, it will impact other code.

Like I said, this is pretty theoretical (and has never caused us a problem), because that part of our API is only used internally.

However in most of the situations I've worked on in the past, the API has been a commodity which we sold to external clients, and we have no control over what they get up to in their code and how they use our API.  I think the ability to protect our code from the wiles of client code, eg: "Adam, your code is erroring"... "no, actually it's your code that caused my code to error...". Whilst the latter might be true, it doesn't really instil confidence in the robustness of our product. Plus from the point of view of self-documenting code, it'd be quite nice to hit a CFC and see the exposed constants like one can with public variables in Java (yes, this is not quite the same thing, but it is an inter-related notion).

Another consideration here is the increased use of named constants makes code easier to read.  Consider this code:

if (currentCurrency == 234){
    // add GST
}

"234"? What sort of magic number is this?  This is a lot clearer:

if (currentCurrency == currency.NZD){
    // give them a discount
}

This doesn't come up in our code that much, but differentiating between language sites comes up reasonably frequently, eg:

if (thisSite == site.FR){
    // show some special page component
}

This makes it clearer as to why this is happening.

As for how to implement it, I think just a CONST keyword should do the trick:

<cfset const this.DEFAULT_CURRENCY = 123>
<!--- or --->
<cfscript>
const this.DEFAULT_CURRENCY = 123;
</cfscript>

That's clear as to its intent, and fits in with the way the current var keyword works (and is mutually exclusive with it, so there'd be no conflict there).

There is an enhancement request already raised for this: 3197154. Vote for or against it if you have an opinion. And also... if you do have an opinion, let me know what it is...

Righto.

--
Adam