Functions are first class citizens

February 6, 2019
I try to bend the internet to my will.
Wizard 32 posts
Followers: 21 people
2

Functions are first class citizens

I try to bend the internet to my will.
Wizard 32 posts
Followers: 21 people
February 6, 2019

In CFML, functions are first class citizens. That means that you can assign functions to variables, pass them around or return them. When support for closures was added in ColdFusion 10, then this meant we could do some interesting things.

Before we dive into the code, I want to know my code is doing what I want so have written some tests. Normally I’d use TestBox for unit testing, but to keep things simple for this post, I’ve created a assert function that checks that the values match and shows ‘PASS’ or ‘FAIL’. It looks like this:

function assert(expected, actual) {
    if (expected == actual) {
        writeoutput('PASS - expected #expected# got #actual#<br>');
    } else {
        writeoutput('FAIL - expected #expected# got #actual#<br>');
    }
}

Consider this simple script to calculate bonuses.

function stockBrokerBonus(salary) {
    return 2 * salary;
}
function cleanerBonus(salary) {
    return 0.1 * salary;
}

// TEST CODE - check expected vs actual
assert(200, stockBrokerBonus(100));
assert(10, cleanerBonus(100));

When I run this I get the following output:

PASS - expected 200 got 200
PASS - expected 10 got 10

As you can see from the passing tests, the code is working nicely, but soon we’ll need to create functions for calculating the bonuses for receptionists, accountants, developers, designers etc (obviously the developers should get the biggest bonus!).

Instead of writing a whole bunch of functions that do factor * salary we can create a function to generate other functions. Using our tests we can refactor without fear of breaking working code.

function bonusCalculator(factor) {
    return function(salary) {
        return factor * salary;
    }
}

stockBrokerBonus = bonusCalculator(2);
cleanerBonus = bonusCalculator(0.1);

// TEST CODE - check expected vs actual
assert(200, stockBrokerBonus(100));
assert(10, cleanerBonus(100));

The tests still pass, and now adding the developer bonus function is as easy as:

developerBonus = bonusCalculator(20);

assert(2000, developerBonus(100));

So far we’ve looked at returning a function from a function. As I mentioned at the start, you can also pass functions in as arguments.

Here’s another simple script that checks to see if a value is ‘truthy’.

function isTruthy(value) {
    return value ? true : false;
}

assert(true, isTruthy(true));
assert(true, isTruthy("TrUe"));
assert(true, isTruthy("YES"));
assert(true, isTruthy(1));
assert(true, isTruthy(-99));
assert(false, isTruthy(0));
assert(false, isTruthy(false));
assert(false, isTruthy("NO"));

Not much to see here. The tests are all passing so we can move on.

I’d also like to have an isFalsey function. I could copy the isTruthy function and switch the result around, but where’s the fun in that Instead we could create a new function that does the inverting of a true to false and a false to true result.

function invert(fn) {
    return function(value) {
        return !fn(value);
    };
}

function isTruthy(value) {
    return value ? true : false;
}

isFalsey = invert(isTruthy);

assert(true, isTruthy(true));
assert(false, isFalsey(true));
assert(false, isFalsey(1));
assert(true, isFalsey(0));

The isTruthy function is unchanged. I have added a new function called invert which accepts a function as an argument (named fn). Thatfn function being passed as an argument can be called (fn(value)) and the result flipped using !.

To create the isFalsey function, I pass the isTruthy function into invert which returns a new function which I
assign to the isFalsey variable. The test all pass so we know it’s working as expected.

The invert function is working well with methods that accept a single value like the isTruthy function does. However, we have a new requirement – to be able to check that two values are equal.

Here’s the new isEqual function.

function isEqual(value1, value2) {
    return value1 == value2;
}

assert(true, isEqual(1, 1));
assert(false, isEqual(1, 2));

A quick change to the invert function to leverage argumentCollection means that it can accept multi arguments. (Note that the /* ...args */ doesn’t actually do anything it’s just a visual clue to show multiple arguments can be passed in).

function invert(fn) {
    return function(/* ...args */) {
        return !fn(argumentCollection=arguments);
    };
}

function isTruthy(value) {
    return value ? true : false;
}

isFalsey = invert(isTruthy);

assert(true, isTruthy(true));
assert(true, isTruthy("TrUe"));
assert(true, isTruthy("YES"));
assert(true, isTruthy(1));
assert(true, isTruthy(-99));
assert(false, isTruthy(0));
assert(false, isTruthy(false));
assert(false, isTruthy("NO"));
assert(false, isFalsey(true));
assert(false, isFalsey(1));
assert(true, isFalsey(0));

Run the tests, and yes, they are still passing. Let’s go ahead and add an isNotEqual function using the invert function to create a new function to flip the result of isEqual. I’m assigning the new function to the isNotEqual variable.

function isEqual(value1, value2) {
    return value1 == value2;
}

assert(true, isEqual(1, 1));
assert(false, isEqual(1, 2));

isNotEqual = invert(isEqual);

assert(true, isNotEqual(1, 2));
assert(false, isNotEqual(1, 1));

// other tests not shown for brevity

That’s it. All the tests pass.

Being able to pass functions around means we can do some interesting things, avoid duplication and write less code.

Comments (2)
2019-02-07 14:58:24
2019-02-07 14:58:24

Thank you for another nicely written article. I like posts like this because they remind developers of techniques they might have forgotten about as well as help advance devs who aren’t as up to speed as they should be.

By the way @aliaspooryorik, I think you should take full credit for your articles under your real name, whoever you are mystery Shakespeare guy!

Like
(1)
(1)
>
Gary Fenton
's comment
2019-02-07 19:56:12
2019-02-07 19:56:12
>
Gary Fenton
's comment

Thanks for the comment, glad you like the posts – I’ve not quite got prolific writing output of the bard just yet, but hope to write a few more posts as I think of things I think may be of interest!

Like
Add your comment