Friday 10 April 2015

Lucee 5 beta: class constructor

G'day:
This article stems from me being slow on the uptake. In the hope I'm not the only thick person out there, I'm gonna document my intellectual failings in case there are other daft ppl out there.



Yesterday I was messing around with some code, and saw what I thought was a bug in Lucee 5 (spoiler: it's not). Here's the code:

// Number.cfc
component {
    static {
        number = 0;
    }

    public void function setNumber(number){
        Number::number = number
    }
    public numeric function getNumber(){
        return Number::number
    }
}

// number.cfm
number1 = new Number()
number2 = new Number()

dump(var={
    number1 = number1.getNumber(),
    number2 = number2.getNumber()
}, label="Initial state")

number1.setNumber(42)

dump(var={
    number1 = number1.getNumber(),
    number2 = number2.getNumber()
}, label="After updating via number1")

dump(var={
    number1 = Number::number,
}, label="Direct access")

When running this code the first time, we get this:


And on subsequent loads, this:


The first time the class is loaded, the static number variable gets initialised to zero; on subsequent loads the variable continues to maintain its current value. This is what one would expect from a static class variable: it persists for the life of the class (which is the life of the JVM, or until the CFC needs to be recompiled due to a change to its source code).

Whilst troubleshooting something else, I ran this variation of the CFC instead:

// AnotherNumber.cfc
component {

    static.number = 0;
    
    public void function setNumber(number){
        AnotherNumber::number = number
    }
    public numeric function getNumber(){
        return AnotherNumber::number
    }
}

// anotherNumber.cfm
number1 = new AnotherNumber()
number2 = new AnotherNumber()

dump(var={
    number1 = number1.getNumber(),
    number2 = number2.getNumber()
}, label="Initial state")

number1.setNumber(42)

dump(var={
    number1 = number1.getNumber(),
    number2 = number2.getNumber()
}, label="After updating via number1")

dump(var={
    number1 = Number::number,
}, label="Direct access")


Surprisingly (only for me, hopefully), this way of setting the static variable behaved differently. The output was always this:


IE: the value always gets reset.

Clearly I didn't think this through too much, because obviously it'll get reset every request, because my code asks it to be. Any code outside of methods in a CFC is run every time and instance is created, and because I'm creating instances here, then static.number = 0 gets re-run each time. Duh.

But wait... so why is this way of assigning a static variable not running each time:

static {
    number = 0;
}

It's because I'm misunderstanding the code. The static{} block is not simply shorthand for bulk setting static variables, which is what I thought it was. No. It's the static constructor (or class constructor). So where all code outside of methods in a CFC is the default constructor for the object (generally referred to as the pseudo-constructor), this static{} block is the equivalent, but for stuff that is only run when the class is initialised, rather than an object instance of the class is initialised.

So static{} gets run once when the given component is first referenced. Coincidentally this could be when an object is created, or it could simply be when a static resource (variable or method) is accessed for the first time.

Let's have a look at it in action:

// Baseline.cfc
component {

    logger = new Logger()
    echo("statement in the pseudo-constructor<br>")
    logger.writeToLog(file="Baseline", text="statement in the pseudo-constructor")

    static {
        echo("statement in the static constructor<br>")
        static.logger = new Logger()
        static.logger.writeToLog(file="Baseline", text="statement in the static constructor")
        static.myVar = 17
    }

    static function f(){
        echo("statement in the static method<br>")
        static.logger.writeToLog(file="Baseline", text="statement in the static method")
    }

    function init(){
        echo("statement in the object constructor<br>")
        logger.writeToLog(file="Baseline", text="statement in the object constructor")
    }

}

Logger is just a wrapper for <cflog>. I need to do this because:

  • LDEV-267: writeLog() broken in Lucee 5
  • LDEV-269: Static constructor not supported in tag-based CFCs
  • LDEV-270: Static constructor inappropriately suppresses output
Basically I need to log the stuff to file as the static{} block suppresses all output, but writeLog() is borked, and I can't make the CFC a tag-based one because tag-based CFCs don't seem to support static constructors.

FFS.

Anyway, if I run that code with this test rig:

// baselineCreatingObject.cfm
baseline = new Baseline();

Then I get these log entries:

statement in the static constructor
statement in the pseudo-constructor
statement in the object constructor


This is predictable: first the class is initialised, then the object pseudo-constructor is run, then the object constructor is run. If I reload the request, I just get this:

statement in the pseudo-constructor
statement in the object constructor


Because the class is initialised by this point.

Next I try accessing a static variable:

// baselineAccessingStaticVariable.cfm
Baseline::myVar;

Result:
statement in the static constructor

So note that no object is created here, so only the class constructor is executed.

Finally, running a static method:

// baselineAccessingStaticMethod.cfm
BaselineAccessingStaticMethod::f();

Result:

statement in the static constructor
statement in the static method


Again: as expected.

Initially I baulked at the syntax of:

static {
    // stuff goes here
}

As it seems a bit of an odd way of defining a constructor, and at odds with the rest of CFML. What I was expecting to see was something like this:

public static function NameOfClass(){
    // stuff goes here
}

As that's more in-keeping with the rest of CFML (as much as where there's an analogy, anyhow), and indeed static-usage: one generally prefixes static access with the name of the class, eg: NameOfClass:myStaticMethod() or NameOfClass::myStaticVariable. It'd also just be more obvious what's going on. I'd've never got confused if the static constructor was done via a method.

However there is a precedent set for the way Lucee does it: in Java: "Initializing Fields › Static Initialization Blocks". So perhaps fair enough I guess. On the other hand, C# does it with a "method": "Static Constructors (C# Programming Guide)". There's a discussion about this against the ticket "268: Static keyword doesn't work for variables" (Update 29/4/2015: this ticket seems to have not been migrated from BitBucket to Jira, so there is no link to that any more). If you have an opinion... pls do add it!

Anyway, I've run out of lunchtime so I'll stop here. I've got another thing to check on this, and will augment this article once done. But this'll be tomorrow, now.

Actually I just found time to do it. The last thing I wanted to check was whether Lucee supported multiple static{} blocks like Java does. In short: yes it does. This works fine:

// Multiple.cfc
component {
    static {
        static.first = true;
    }
    static {
        static.second = true;
    }
}

// multiple.cfm
dump([Multiple::first, Multiple::second])

It outputs:


Good work, Lucee Team.

Righto.

--
Adam