Fat-Arrow Functions And Lambda Expressions Are Supported In Lucee 5.3.2.77
Yesterday morning, I spent a few hours combing through the Lucee 5.3 documentation looking for features that would be worth digging into a bit further. And, one of the tasty little treats that I came across is the fact that Fat-Arrow Functions and Lambda Expressions are supported in Lucee's version of ColdFusion. Coming from the JavaScript and TypeScript worlds, fat-arrow functions provide an elegant syntax that I've grown to love. As such, I just wanted to get a sense of how fat-arrow functions and lambda expressions would look and feel in Lucee CFML.
CAUTION: The term "lambda expression" most likely means something more in the mathematical sense than the way in which I am using it. When I say "lambda expression", I mean it in the most colloquial sense possible - the way the majority of JavaScript developers, with no background in mathematics or Functional Programming - mean it.
In the Lucee CFML engine, the fat-arrow function is really just a short-hand syntax for defining anonymous functions / closures. So, instead of writing something like this:
<cfscript>
greet = function( name ) {
return( "Hello, #name#." );
};
echo( greet( "Kim" ) );
</cfscript>
... you can writing something like this:
<cfscript>
greet = ( name ) => {
return( "Hello, #name#." );
};
echo( greet( "Kim" ) );
</cfscript>
Notice that we've dropped the function
keyword and added the =>
notation. Now, if you you want to trade-off some readability for some terseness, you can even do something like this:
<cfscript>
greet = ( name ) => "Hello, #name#.";
greet( "Kim" );
</cfscript>
In this case, we can ditch the {
and }
tokens and just assume that the body of the implied function is also going to act as the implicit return
statement.
To see this in action, I wanted to try and implement a natural sort algorithm using Lucee 5.3 CFML and fat-arrow syntax. This is a fun demo because it's small; but, sufficiently complicated and has ample opportunity for Function expressions.
A "natural sort" is simply a user-friendly sort in which a string, with embedded numbers, sorts the numeric portions of the string as numbers rather than sorting the numbers using an ASCII sort. Which means that an ascending sort of items leaves us with:
Item 1
Item 2
Item 10
... instead of an ASCII sort, which would leave us with:
Item 1
Item 10
Item 2
But, this post isn't about how to implement a rock-solid natural sort algorithm - this post is just about fat-arrow functions in Lucee. So, please take my particular implementation of the natural sort with a grain of salt. Instead, focus on my use of the =>
lambda function syntax:
<cfscript>
// When we pass an Array-of-Structs into the sorter, we can provide a Struct path as
// the accessor.
dump(
naturalSortOn(
[
{ name: "Embedded 233 value" },
{ name: "Embedded 10 value" },
{ name: "Embedded 1 value" },
{ name: "Embedded 24.00.23 value" }, // Not a valid number.
{ name: "Embedded 2.5 value" },
{ name: "Embedded 24.2 value" },
{ name: "Embedded -5 value" }
],
"name" // Accessor struct path.
)
);
// When we pass an Array-of-Strings into the sorter, there is no "struct path" to
// use. As such, we have to provide an accessor that just echoes the item.
dump(
naturalSortOn(
[
"233 leading value",
"10 leading value",
"1 leading value",
"24.00.23 leading value", // Not a valid number.
"2.5 leading value",
"24.2 leading value",
"-5 leading value"
],
( item ) => item // Accessor function.
)
);
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I sort the given collection using a "natural sort". That is, embedded numeric
* values are sorted AS NUMBERS as much as possible.
*
* If the ACCESSOR is a string, the collection will be treated as an Array of
* Structs and the ACCESSOR will be treated a a struct-path.
*
* If the ACCESSOR is a Function / Closure, the collection will be treated as a black
* box and the ACCESSOR will be used to locate the operand.
*
* @collection I am the collection being sorted.
* @accessor I am the means by which the item operands are located.
* @output false
*/
public array function naturalSortOn(
required any collection,
required any accessor
) {
// In order to normalize the identification of the operand, we want it to always
// be a Function. As such, if the provided access is a simple value (string),
// then we're going to assume it's a Struct Path and generate a plucker function.
var extractOperand = isSimpleValue( accessor )
? ( item ) => item.find( accessor )
: accessor
;
// I take the target string (on which we are sorting) and mask any embedded
// numbers such that they are roughly the same length. This will allow the
// subsequent alphabetical sort to work without worrying whether or not the
// content contains numbers.
var normalizeEmbeddedNumbers = ( value ) => {
// Converts inputs like "aaa-111-bbb" to "aaa-.0000000111.0000000000-bbb".
var normalizedValue = lcase( value )
.reMatch( "(?x) -?\d+(\.\d+)* | \D+" )
.map(
( segment ) => {
if ( isNumeric( segment ) ) {
// In an attempt to properly handle negative numbers,
// positive numbers will be prefixed with a ".", which is
// sorted right after "-" in the ASCII TABLE.
var prefix = ( segment < 0 )
? ""
: "."
;
return( prefix & numberFormat( segment, "0000000000.0000000000" ) );
} else {
return( segment );
}
}
)
.toList( "" )
;
// For the sake of the demo, let's write this normalized value to the system
// log so that we can see which value is being sorted under the hood.
systemOutput( normalizedValue );
return( normalizedValue );
};
var sortedCollection = collection
// To reduce the amount of processing that the sort has to perform, we want
// to prepare the collection for sort ahead of time. To do this, we're going
// to map the input collection onto an transient collection that presents a
// static value that we can use for the sort comparison (the operand).
.map(
( item ) => {
return({
operand: normalizeEmbeddedNumbers( extractOperand( item ) ),
input: item
});
}
)
// Once the input collection has been prepared for sort, we can now perform
// an alphabetical sort on the static operand.
.sort( ( a, b ) => compare( a.operand, b.operand ) )
// Now that the transient collection has been sorted on the static operand,
// we can map the transient collection back to original inputs.
.map( ( item ) => item.input )
;
return( sortedCollection );
}
</cfscript>
As you can see, I am using the fat-arrow function syntax in two ways:
- As a short-hand for passing Functions to collection operations.
- As a short-hand for assigning Function references.
Now, when we run this Lucee CFML code, we get the following output:
As you can see, the numbers embedded within the input strings were treated as "numbers" and just such as a collection of ASCII characters.
To do this, I am finding the numbers within the strings and attempting to normalize them before applying the ASCII sort. You can see this if we look at the system output, where I am logging the normalized values that I generate under the hood:
As you can see, every embedded number gets padding with 0
digits before and after the decimal.
This is a small feature; but, I am very excited about seeing it in Lucee 5.3.2.77. Every time my ColdFusion markup can look a little more like JavaScript or TypeScript, I am a happy coder. If nothing else, this will reduce the amount of noise that will be required for using the collection-based iteration methods.
Want to use code from this post? Check out the license.
Reader Comments
I just requested ACF support fat arrow functions in the survey they sent out. I'd also love it if there were support for destructuring. I find destructuring super useful, especially when combined with default argument values.
Holy smoke. Lucee 5 has really come on, leaps and bounds. It's going to be like writing TypeScript, soon!
There is one bit, I don't quite understand. I can see that 'name' is being extracted and used as the operand for sorting the array of Structs, but I don't see how the array of simple values is being sorted, unless you are doing something like:
Trying to keep track of functions inside of functions, ties my mind into knots!
However, it is good brain training, nevertheless. Like a cryptic crossword puzzle for programmers.
And one other thing. Is a Lambda function another name for a fat arrow function?
And one final final thing!
What is:
Never seen this before!
@Chris,
The fat-arrow stuff is just noice. When it was first introduced to JavaScript, I was a bit hesitant. But, once I started using it, going back to the
function()
nomenclature feels so verbose.One thing I should mention that I didn't demonstrate in this post is that you can still define the full argument declaration, even with fat-arrow syntax. So, instead of just having something like:
... I could have something like this:
Notice that my argument now has
required struct
meta-data. Of course, since the=>
syntax is used as a local expression, omitting the argument meta-data feel "OK" to me. The Function itself is so specific, contextual, and short-lived, that it feels self-documenting enough to not have all the argument semantics in place.@Charles,
Re: the sorting, you are right. If you look at the second argument for the String-based sort, I have this:
This is a short-hand notation for this:
Notice that in the first version,
item
is the impliedreturn
value. And, in the second form, I am explicitly returning it. So, for the collection of Strings, the value returned from the access is the String value itself.Then, inside the sort function, I normalize the "accessor" to always be a Function:
So, if I pass in a "name", the name becomes a Function that plucks the name property. And, if I pass in a Function (as in the String-based collection), it gets used as-is.
@Charles,
Re:
In my mind, Yes - I use the two interchangeably. But, I think at a technical level, they may have different (but overlapping) implications. I think it depends on how academic you want to get :D
I think that "fat-arrow" implies the syntax and the run-time binding (at least in TypeScript). And I think the "lambda" implies a bit more academically to how the function is used and whether or not it is "pure". But, like I said, I don't really have a low-level understanding .... so to my simple caveman brain, they are interchangeable :D
@Charles,
systemOutput()
just writes to the standard-output stream of the application instance. If you are using something like CommandBox, you can view it by running:RE: Lamda Functions
OK. Great. I know enough about fat arrow functions from my JavaScript knowledge. The main reason I use these, is so that I can use the parent function's THIS context. I presume this is the case, in Coldfusion as well?
@Charles,
I don't think the fat-arrow function would have the same type of
this
-specific binding in Lucee ColdFusion. Meaning, I don't think there is any difference between this:and this:
I believe that the
()=>
syntax is nothing more than a short-hand for thefunction()
closure syntax.That said, Functions in ColdFusion do execute inside of a "Page Context"; and the rules around how Closures work with Page Context are not 100% clear to me. I believe that a Closure is bound to the Page Context in which it is defined since Closures are able to access the implicit
variables
scope of the page in which they were defined.@All,
In this post, I implemented the "natural sort" using an intermediary array, which means that the original array and the sorted array are two different objects. Yesterday, I needed to implement an in-place sort using a natural sort order. So, this morning I took a look at how you could tweak the algorithm:
www.bennadel.com/blog/3872-performing-an-in-place-natural-sort-on-an-alpha-numeric-array-in-lucee-cfml-5-3-6-61.htm
It's the same basic idea; only performing an in-place sort and a mapped set of values.