Using The Safe-Navigation Operator To Safely Clean Up Resources In Lucee 5.3.2.77
I know that the "safe navigation" operator has existed in more recent releases of ColdFusion; however, coming from Adobe ColdFusion 10, I haven't been able to use the operator until my team recently switched to using Lucee CFML. As such, I haven't had time to develop consistent patterns around the operator's usage. In fact, before yesterday's post on the performance impact of using Closures in Lucee, I don't think I'd ever actually used the safe-navigation operator in "real" code. In an effort to develop a better mental model for it, I wanted to codify yesterday's pattern of using the safe-navigation operator to safely clean-up resources in Lucee 5.3.2.77.
The safe-navigation operator in ColdFusion - ?.
- will short-circuit the evaluation of an object-access path if the object being accessed is null
. If the expression is short-circuited, the resultant value is null
. If the expression is not short-circuited, the resultant value is the value being accessed at the end of the expression.
The safe-navigation operator allows you to safely evaluate expressions likes this:
return( context ?. object ?. property );
... where context
may be null; or, object
may be null. If either of these is null
, the return()
statement will return null
instead of throwing some sort of "null reference" error. And, of course, if the values are all defined, the .property
value will be returned.
If you're familiar with JavaScript, this is akin to (though not exactly like) calling:
return( context && context.object && context.object.property );
... where each path of the object-access logic is checked before it is evaluated.
With that said, in yesterday's post, I was using the safe-navigation operator to "close" a Jedis resource (returning it to the Redis connection pool) after I was done using it:
<cfscript>
// ....
try {
jedis = jedisPool.getResource();
jedis.set( "hello", "world" );
} finally {
// Once we are done interacting with Redis, we have to close the Jedis resource
// or it won't be returned to the connection pool. Jedis will be NULL if a
// resource could not be obtained from the connection pool.
// --
// NOTE: We are using the safe-navigation operator to safely close the resource
// even if it wasn't obtained.
jedis?.close();
}
</cfscript>
Notice that I am using the ?.
on the jedis
reference. This will allow the .close()
method to be called if jedis
is defined; and, if jedis
is null
, this expression will be short-circuited and evaluate to null
without throwing a null-reference error.
ASIDE: You could probably argue here that the jedis assignment could be moved outside of the
try
block; but the approach that I have here is the approach outlined in the Jedis Getting Started wiki entry.
This pattern is more broadly applicable to any context that creates resources that need to be cleaned-up, even if errors are thrown within the consuming logic. For example, we could use it to close output streams after we are done writing to them:
<cfscript>
message = (
"In the year of our Lord 1314, patriots of Scotland, starving and outnumbered, " &
"charged the fields of Bannockburn. They fought like warrior poets. They fought " &
"like Scotsmen and they won their freedom."
);
try {
outputStream = createObject( "java", "java.io.ByteArrayOutputStream" ).init();
gzipOutputStream = createObject( "java","java.util.zip.GZIPOutputStream" ).init( outputStream );
// Write the message to the GZIP output stream.
messageBytes = charsetDecode( message, "utf-8" );
gzipOutputStream.write( messageBytes, 0, arrayLen( messageBytes ) );
gzipOutputStream.finish();
// Persist the GZIP binary data to a flat file.
fileWrite( "./quote.txt.gzip", outputStream.toByteArray() );
} finally {
// Close all streams once we are done.
// --
// NOTE: In this case, it is extremely unlikely that these objects wouldn't
// exist. However, this is a usage-pattern that I am starting to embrace - using
// the SAFE NAVIGATION OPERATOR to safely close objects that were created within
// the bounds of a try-block.
gzipOutputStream?.close();
outputStream?.close();
}
// FUN SIDE NOTE: You can easily extract this GZIP data using the extract() function:
// --
// extract( "gzip", "./quote.txt.gzip", "./quote.txt" );
</cfscript>
Or, we could use the safe-navigation operator to release a distributed lock once we've completed our synchronized business logic:
<cfscript>
try {
distributedLock = getLock( "myLock" );
// ... do some complicated stuff within this globally-synchronized block.
echo( "Done." );
} catch ( "DistributedLock.LockError" error ) {
echo( "Someone else is already doing this thing!" );
} finally {
// Release the distributed lock once we are done with the synchronized work.
// --
// NOTE: We are using the safe-navigation operator so that we can safely call
// this line of code even if we failed to obtain the distributed lock.
distributedLock?.release();
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I obtain a mock distributed-lock.
*
* @name I am the name of the lock being obtained.
*/
public any function getLock( required string name ) {
if ( randRange( 0, 1 ) ) {
throw( type = "DistributedLock.LockError" );
}
return({
release: () => {
// ... this is just a mock lock.
}
});
}
</cfscript>
Notice that by using the ?.
operator in the lock-release expression, the .release()
method won't get evaluated if the lock was never acquired in the first place.
Anyway, this was mostly a note-to-self as I try to establish patterns of usage around the safe-navigation operator in Lucee CFML 5.3.2.77. If anyone has a use-case for this operator that they see coming up time and again in their own code, I would love to hear about it; I don't yet have any real instinct for the breadth of places in which this operator can be used in ColdFusion.
Want to use code from this post? Check out the license.
Reader Comments
Now I know what:
Is really doing, in Angular!
@Charles,
Ha ha, exactly right! In your case, the
?.
is saying, "ifuser$
is undefined, returnundefined
; or, return.profile
". I believe this operator is also available in TypeScript as well ... but don't quote me on that ;)Actually that raises an interesting point.
What is the name of the template language that Angular uses?
It's not HTML, JavaScript or TypeScript?
I mean the special syntax that we use in Angular HTML pages...
@Charles,
Hmm, I am not sure it has a specific name. Just "Angular template" :D That said, I believe that Angular template are required to be valid HTML in order to parse. So, while the syntax like
[prop]
and(binding)
look strange, the Angular team made sure that they technically valid HTML when choosing which syntax tokens to use.So, I guess, the templates are HTML (as far as language). But, they get - of course - compiled down into something that Angular can render and apply bindings to.
Another use case of safe navigation is to pair it with the Elvis operator to provide a default value or behavior:
assignMe = some?.key?.that?.is?.not?.there?:"I'm a default value!"
Example:
https://trycf.com/editor/gist/aef0b0d98bd2f8c285de388730de6865/lucee5?theme=monokai
There are caveats to this, for instance if you want to treat undefined keys different than null returns (CFML treats them both as null), or if you need to check for empty values when the key exists. However, if it fits the situation, it can dramatically reduce the lines of code and therefore make it much more readable.
@Abram,
I like that a lot. It's a lot like JavaScript's
||
operator for things like:I kind of wish ColdFusion acted more like JavaScript with all of the Truthy / Falsy stuff.
@Ben,
Unrelated, but I've noticed you wrap your returns and multi-line string assignments in parenthesis. Is that just a style choice that carried over from JavaScript, or does this provide some sort of safety in CFML similar to JS?
@Abram,
Just a personal preference :D
Plus, for anything I write for the blog, I try to limit the width of the code to 90-characters. If I go longer than that, then the code-snippets tend to have to be scrolled horizontally (which is not obvious when the code-snippet is taller than the browser viewport). So, in a case like the one in this post, I arbitrarily break the text-line up into three lines so that no single line causes horizontal scrolling.
But, again, this is just me.