Sunday 27 June 2021

CFML: tag-based versions of some script-based code

G'day:

OMFG the things I do for my CFML community colleagues.

I've been asked by a couple of people to show the tag-based version of the script-based CFML code I have been showing as examples when helping people recently. This is so people who are less familiar with CFScript can compare the two, and perhaps get a better handle on the script code.

Editorialisation

I have not written new tag-based code in CFML in probably 15 years, other than when it's been absolutely unavoidable like back before queryExecute existed, so we still needed to use <cfquery> (and similar stuff like <cfhttp>, and what-have-you). I have maintained old tag-based code, but I've been lucky in that I've always been in the position to implement new code using modern practices.

Some CFML History

Since ColdFusion 9 was released in 2009 (that's over a decade ago, yeah?), it's been largely unnecessary to write any business logic in tag-based code, as script-based CFCs were added to the language. The only real relics of tag-only functionality were stuff like the afore-mentioned DB and external system access functionality that was all tags still. But that stuff should be hidden away in adapter CFCs anyhow, so any necessary tag-based code should be well isolated.

It has not been necessary to write CFML in tags at all since 2014 (over seven years ago), when - in ColdFusion 11 - the last bits of tag-only functionality were ported to CFScript.

The only place any tags ought to have been used since then are in views. And really these days your views should probably be being handled by a client-side framework anyhow, so - in my opinion - no new tag-based CFML code should be being written in 2021, and shouldn't have been for over half a decade now. All new CFML code should be written in CFScript. All CFML developers must be fluent in CFScript.

Reality for a lot of people

That's all good in theory, but in practice there is a lot of legacy code out there. We don't all get to choose what codebases we work on daily, and I know some CFML devs don't get to work with modern code much, so: tags it is. And this also means some devs don't get exposed to CFScript as much as they could be, so it could all seem a bit foreign to them. Fair enough.

The code

A week or so ago, I did an exercise "CFML: emulating query-of-query group-by with higher-order functions". The final version of the code for this was (tagsVsScriptDemonstrations/groupByViaCfml/ScriptVersion.cfc):

component {

    public query function groupByYearAndMonth(required query ungroupedRecords) {
        return ungroupedRecords.reduce((grouped={}, row) => {
            var y = row.settlementDate.year()
            var m = row.settlementDate.month()
            var key = "#y#-#m#"
            grouped[key] = grouped[key] ?: {stgl = 0, ltgl = 0}
            grouped[key].stgl = grouped[key].stgl + row.ShortTermGainLoss
            grouped[key].ltgl = grouped[key].ltgl + row.LongTermGainLoss

            return grouped
        }).reduce(
            (records, key, values) => {
                records.addRow({
                    month = key.listLast("-"),
                    year = key.listFirst("-"),
                    ltgl = values.ltgl,
                    stgl = values.stgl
                })
                return records
            },
            queryNew("month,year,ltgl,stgl", "Integer,Integer,Double,Double")
        ).sort((r1, r2) => {
            var yearDiff = r1.year - r2.year
            if (yearDiff != 0) {
                return yearDiff
            }
            return r1.month - r2.month
        })
    }
}

I think a direct analogue of this in tags would be (tagsVsScriptDemonstrations/groupByViaCfml/TagsVersion.cfc)

<cfcomponent output="false">

    <cffunction name="groupByYearAndMonth" returntype="query" access="public">
        <cfargument name="ungroupedRecords" type="query" required="true">

        <cfset grouped = structNew()>
        <cfloop query="ungroupedRecords">
            <cfset var y = year(settlementDate)>
            <cfset var m = month(settlementDate)>
            <cfset var key = "#y#-#m#">

            <cfif not structKeyExists(grouped, key)>
                <cfset grouped[key] = structNew()>
                <cfset grouped[key].stgl = 0>
                <cfset grouped[key].ltgl = 0>
            </cfif>
            <cfset grouped[key].stgl = grouped[key].stgl + ShortTermGainLoss>
            <cfset grouped[key].ltgl = grouped[key].ltgl + LongTermGainLoss>
        </cfloop>

        <cfset var records = queryNew("month,year,ltgl,stgl", "Integer,Integer,Double,Double")>
        <cfloop collection="#grouped#" item="local.key">
            <cfset queryAddRow(records)>
            <cfset querySetCell(records, "month", listLast(key, "-"))>
            <cfset querySetCell(records, "year", listFirst(key, "-"))>
            <cfset querySetCell(records, "ltgl", grouped[key].ltgl)>
            <cfset querySetCell(records, "stgl", grouped[key].stgl)>
        </cfloop>
        <cfset querySort(records, sorter)>

        <cfreturn records>
    </cffunction>

    <cffunction name="sorter" returntype="numeric" access="private">
        <cfargument name="r1" required="true">
        <cfargument name="r2" required="true">

        <cfset var yearDiff = r1.year - r2.year>
        <cfif yearDiff NEQ 0>
            <cfreturn yearDiff>
        </cfif>

        <cfreturn r1.month - r2.month>
    </cffunction>

</cfcomponent>

I'm not going to go through and cross-annotate anything, but I've used analogous variable names, and kept the logic in the exact order where I could. I've also tried to keep the same level of verboseness (or lack thereof) in both examples, so that it's as true to a like-for-like as I can muster. BTW I'm also not using any member functions or other newer CFML constructs / features in these examples.


John Whish gave me a good exercise to do this morning which I'll also reproduce here. In this example we're taking an array, and deriving the two-element combinations of all the elements. For example if we start with this: ["A", "B", "C", "D", "E"], the expected result would be this: ["AB", "AC", "AD", "AE", "BC", "BD", "BE", "CD", "CE", "DE"]

In CFScript it's this (tagsVsScriptDemonstrations/combinations/ScriptVersion.cfc):

component {

    public array function getCombinations(required array array) {
        var working = duplicate(array)
        return array.reduce((combinations=[], prefix) => {
            working.deleteAt(1)
            return combinations.append(working.map((element) => "#prefix##element#"), true)
        })
    }
}

And the tag version (tagsVsScriptDemonstrations/combinations/TagsVersion.cfc):

<cfcomponent output="false">

    <cffunction name="getCombinations" returntype="array" access="public" output="false">
        <cfargument name="array" type="array" required="true">

        <cfset var working = duplicate(array)>
        <cfset var combinations = arrayNew(1)>
        <cfloop array="#array#" item="local.prefix">
            <cfset arrayDeleteAt(working, 1)>
            <cfset var subCombinations = arrayNew(1)>
            <cfloop array="#working#" item="local.element">
                <cfset arrayAppend(subCombinations, "#prefix##element#")>
            </cfloop>
            <cfset arrayAppend(combinations, subCombinations, true)>
        </cfloop>
        <cfreturn combinations>
    </cffunction>

</cfcomponent>

As a last example, I decided to see if I could port the actual test class for the combinations exercise to tags. And - yes - I could. It's really clumsy, but it works. First here's the original script version (tagsVsScriptDemonstrations/combinations/CombinationsTest.cfc):

import testbox.system.BaseSpec
import cfmlInDocker.miscellaneous.tagsVsScriptDemonstrations.combinations.ScriptVersion
import cfmlInDocker.miscellaneous.tagsVsScriptDemonstrations.combinations.TagsVersion

component extends=BaseSpec {

    function beforeAll() {
        variables.testArray = ["A", "B", "C", "D", "E"]
        variables.expectedCombinations = [
            "AB", "AC", "AD", "AE",
            "BC", "BD", "BE",
            "CD", "CE",
            "DE"
        ]
    }

    function run() {
        describe("Testing script version", () => {
            it("returns the expected combinations", () => {
                var sut = new ScriptVersion()
                var result = sut.getCombinations(variables.testArray)

                expect(result).toBe(variables.expectedCombinations)
            })
        })
        describe("Testing tags version", () => {
            it("returns the expected combinations", () => {
                var sut = new TagsVersion()
                var result = sut.getCombinations(variables.testArray)

                expect(result).toBe(variables.expectedCombinations)
            })
        })
    }
}

And the tags version (tagsVsScriptDemonstrations/combinations/CombinationsTestUsingTags.cfc):

<cfimport path="testbox.system.BaseSpec">
<cfimport path="cfmlInDocker.miscellaneous.tagsVsScriptDemonstrations.combinations.ScriptVersion">
<cfimport path="cfmlInDocker.miscellaneous.tagsVsScriptDemonstrations.combinations.TagsVersion">

<cfcomponent extends="BaseSpec" output="false">

    <cffunction name="beforeAll">
        <cfset variables.testArray = ["A", "B", "C", "D", "E"]>
        <cfset variables.expectedCombinations = [
            "AB", "AC", "AD", "AE",
            "BC", "BD", "BE",
            "CD", "CE",
            "DE"
        ]>
    </cffunction>

    <cffunction name="run">
        <cfset describe("Testing script version", testingScriptVersionHandler)>
        <cfset describe("Testing tags version", testingTagsVersionHandler)>
    </cffunction>

    <cffunction name="testingScriptVersionHandler">
        <cfset it("returns the expected combinations", returnsTheExpectedCombinationsScriptVersionHandler)>
    </cffunction>

    <cffunction name="returnsTheExpectedCombinationsScriptVersionHandler">
        <cfset var sut = new ScriptVersion()>
        <cfset var result = sut.getCombinations(variables.testArray)>

        <cfset expect(result).toBe(variables.expectedCombinations)>
    </cffunction>

    <cffunction name="testingTagsVersionHandler">
        <cfset it("returns the expected combinations", returnsTheExpectedCombinationsTagsVersionHandler)>
    </cffunction>

    <cffunction name="returnsTheExpectedCombinationsTagsVersionHandler">
        <cfset var sut = new TagsVersion()>
        <cfset var result = sut.getCombinations(variables.testArray)>

        <cfset expect(result).toBe(variables.expectedCombinations)>
    </cffunction>

</cfcomponent>

Yikes.


And indeed "yikes" was my reaction to each of those examples. The tag-based code is just full of unnecessary and obstructive bloat, and just a mess to read. And a bit clunky to implement.

Ugh. However if there's any other code I've done recently that you'd find helpful to read as tag-based code, let me know, and I'll see if I can do a port. But the quid pro quo is that if yer currently still writing CFML in tags, and have it within your control to stop doing that and join the direction CFML has been taking since mid-last-decade… please try to move on.

PS: also I'm intending to do another article that takes a more focused look on understanding how CFML's collection-iteration higher-order functions (you know; Array.map, Struct.reduce, Query.filter etc) work, and comparing back to tag-based implementations.

Righto.

--
<cfadam />