Tuesday 7 April 2015

Lucee 5 beta: static methods & properties

G'day:
So Lucee 5 beta is out, and I decided to have a look at some stuff this evening.

One of the good things in Lucee 5 is that it will support static methods and properties. This should have been in CFML since CFMX6, so it's high time it was added to the language.

I'm gonna have a quick look at how this stuff has been implemented.

As a baseline, what is a static method? A static method is one which doesn't require an object for it to be called upon; it can be called upon the CFC itself. If you think about it, it's reasonably often one might have a helper method which belongs in a component, but doesn't actually act on an object, it just takes some arguments, applies a behaviour to them and returns a result. To date in CFML one always needed to create an object before calling a method. No longer.

Here's an old-school implementation of a temperature converter:

// Conversion.cfc
component {

    public numeric function celsiusToFahrenheit(required numeric t) {
        return t / 5 * 9 + 32
    }

}

That's straight forward. To use this though, one needs to create an object:

// conversion.cfm
conversion = new Conversion()
boiling.celsius = 100
boiling.fahrenheit = conversion.celsiusToFahrenheit(boiling.celsius)
dump(boiling)

Result:


That's fine. However one needs to create an object to use the method. But why? The method doesn't act on the object's state. Indeed this object has no state. It's just a simple library holding a helper function.

In other languages, one would simply call the method against the component, eg:

boiling.fahrenheit = Conversion.celsiusToFahrenheit(boiling.celsius)

Where Conversion there is a reference to the component, not an instance of it.

Well now in Lucee one can do this.

Here's how it should (but currently does not) work:

// Conversion.cfc
component {

    public static numeric function celsiusToFahrenheit(required numeric t) {
        return t / 5 * 9 + 32
    }

}


// conversion.cfm

boiling.celsius = 100
boiling.fahrenheit = Conversion::celsiusToFahrenheit(boiling.celsius)

dump(boiling)

Note that one does not use the dot-operator for accessing static methods, one uses the :: operator instead. I dunno why they decided to do this... it's a bit PHP-ish in my view. I suppose it's not too late to raise an E/R to just use the dot operator here instead. Unless it's some fundamental drawback of dynamic languages that needs to distinguish more emphatically between object methods and static methods?

Update:

The issue detailed below in Conversion.cfc has been fixed (within 24h!) in Lucee 5.0.0.43 (I was testing on .42). Good work Micha & The Team. So it's important to note the syntax above does work now.

But, anyway, it doesn't work. Output:


I wondered messed around with the syntax a bit, and in the process removed the type qualifier, instead having this:

// Conversion.cfc
component {

    public static function celsiusToFahrenheit(required numeric t) {
        return t / 5 * 9 + 32
    }

}

And this errored with:


At least it's seeing the static syntax now, even if it's... wrong. So that sux. And given I based my example on the example in their own docs, I dunno how this slipped through QA. Still: it's only a beta, I guess (this is kinda alpha quality, but still).

Using an alternative syntax, this can be made to work though:

// Conversion.cfc
component {

    static {
        public numeric function celsiusToFahrenheit(required numeric t) {
            return t / 5 * 9 + 32
        }
    }

}

This now works. I hope we're not stuck with that syntax though, as it's a bit rubbish. static should just be a keyword modifier on the method or property declaration. One should not need to put it in curly braces like that.

How about static properties? As well as methods, one should be able to have static properties. These are properties that all object instances of the given component share:

// 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")

This is summed up by the output:


Note that both number1 and number2 share the same number value. I only updated number for number1, but it's also reflected in the dump for number2. On reflection my variable / class naming is a bit confusing there. Oops.

One thing to note here is that static property values last for the life of the component, which is to say for the life of the JVM. If I refresh that page, I get this:


This might not seem to make sense initially, but it does (make sense). It def caught me out initially.

I'm using accessor methods here, but one doesn't have to. And one can set a static property to be public (default) or private:

// Accessibility.cfc
component {

    static {
        accessible = 17
        private inaccessible = 19
    
        function getInaccessible(){
            return Accessibility::inaccessible
        }
    }

}

// accessibility.cfm
echo("Publicly accessible: #Accessibility::accessible#<br>")

try {
    echo("Not publicly accessible: #Accessibility::inaccessible#<br>")
}catch (any e){
    echo("#e.message# #e.detail#<br>")
}
echo("Accessible via method: #Accessibility::getInaccessible()#<br>")

This outputs:

Publicly accessible: 17
Component from type [github-scratch.blogExamples.lucee.5.cfcs.static.Accessibility] has no accessible static Member with name [INACCESSIBLE]
Accessible via method: 19


So that's all quite good.

OK, so there was a bit of a false start there (issue LDEV-254), and some ropey syntax requirements as a result, but I think once they iron that glitch out, they've got a good implementation here. I will say though, I think that's the fastest I have ever found a bug in CFML: beating Adobe is quite an achievement there, Lucee! Oops.

Update:

I found another issue in that interfaces ignore static requirements. It was not worth writing up here, but the details are on the ticket: LDEV-253.

Tomorrow, I'm gonna try to have a look at lambda expressions.

Righto.

--
Adam