Saturday, 11 October 2014

CFML: emulating generators with closure

G'day:
Half way through my other article this morning ("PHP: Inspired by Duncan: palindromes via generators"), I was thinking how cool generators are, and how it sucks CFML doesn't have them (go vote, btw: RAILO-2942; and for ColdFusion: 3555025).

Then I had a watershed moment.

I worked through a bit of code in my head, and concluded I could most likely emulate a generator - for the most part - using closure.

So I set to to convert my PHP code to CFML, and use closure for the generator:

function createPalindromeSequence(max=-1){
    var n                = 0
    var reserveStack    = []
    var shortPalindrome    = 0

    return function(){
        n = javacast("string", n)
        shortPalindrome = n & n.reverse().mid(2, n.len())
        if (reserveStack.len() && shortPalindrome > reserveStack[1]){
            var nextOne = reserveStack[1]
            reserveStack.deleteAt(1)
            return nextOne
        }
        if (max != -1 && shortPalindrome > max) return;
        n++
        n = javacast("string", n)
        reserveStack.append(n & n.reverse())
        return shortPalindrome
    }
}

And it only bloody works! Pleasingly, I also got it right first time (ie: from what I planned in my head to working code was just a matter of dumping the brain out into a file); other than a parser shortcoming in Railo that I didn't predict.

The thing with generators is that between calls to them, they recall where they were at the previous time around, and just resume where they left off. A generator returns a result with the yield keyword, and the next time the generator is called, processing continues on the statement after the yield. That's cool, but impossible in CFML. However with some slight logic tweaks, and putting some variables in the calling function's context so the closure function can "remember them", we can emulate that quite nicely.

I'm annoyed at having to use javacast() in there: I was hoping I could do this:

"#n#".reverse()

But - strange for Railo which is usually pretty good at understanding "strings" and "numbers" as being different, it insists that "#n#" is actually a number (which it clearly is not).

I also wish Array.deleteAt() returned the element which was deleted (like Array.shift() might), but it doesn't. So there's a two-step there.

Those gripes are small, and I like that code.

How does it perform against PHP? I created a test rig much like I did in the previous article, and also made a version of this which would run on ColdFusion 11 and tested both. euler36.cfm and euler36_coldfusion.cfm respectively.

Typical timings were:

Railo: 41ms
ColdFusion: 1159ms

So sorry CFML: you're quite a bit slower than PHP. And ColdFusion: bleah. Over a second just to do that processing? That's dog slow. The CFML code is still much nicer than the PHP code though.

Anyway, there you go. I guess I still write about CFML after all.

Righto.

--
Adam