Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Using Closures To Encapsulate FileReadLine() Operations In Lucee CFML 5.3.6.61

By Ben Nadel on
Tags: ColdFusion

In Episode 75 of the Modernize-or-Die podcast, Brad Wood talked about the fact that the fileReadLine() function in Lucee CFML was thread-safe; but, that you must remember to call fileClose() once you are done otherwise the files can remain locked. These days, whenever I hear "you have to remember" in a programming context, I think to myself, let's wrap that in a ColdFusion Closure so that I don't have to remember anything. This is a pattern that I've been using - and loving - more and more in my ColdFusion code. As such, I thought it would be fun to take a quick look at wrapping fileReadLine() operations in a ColdFusion Closure so that the file management is completely encapsulated in Lucee CFML 5.3.6.61.

The idea behind this approach is that we use a Closure to define what we want to do with a given resource; but, do so in such a way that we don't have to care about how that resource is being obtained. Instead, we just pass the Closure to another function whose responsibility it is to manage the resource and then invoke our closure once that resource is available.

This technique is how I manage all Redis resources in ColdFusion; and, it's the same technique we can apply to the fileOpen() / fileReadLine() / fileClose() choreography.

To demonstrate this, let's quickly create a small, Newline-Delimited JSON (ND-JSON) file, which is just a file that aggregates a series of separate JSON (JavaScript Object Notation) payloads:

<cfscript>
	
	buffer = [];

	for ( i = 1 ; i <= 10 ; i++ ) {

		buffer.append(
			serializeJson({
				value: i
			})
		);

	}

	fileWrite( "./data.ndjson", buffer.toList( chr( 10 ) ) );

</cfscript>

As you can see, our ND-JSON file is exactly what the file-extension means: JSON payloads separated by a newline-character.

Now, let's iterate over this ND-JSON file using the fileReadLine() method; but, using a ColdFusion Closure so that we don't have to manage the low-level file operations:

<cfscript>

	// Iterate over the ND-JSON (Newline-Delimited JSON) file, record by record.
	processNdJsonFile(
		"./data.ndjson",
		( data ) => {

			echo( "Processing data value: #data.value#. <br />" );

		}
	);

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I iterate over the given ND-JSON (Newline-delimited JSON) file, deserializing and
	* passing each record to the given callback for consumption.
	* 
	* @filePath I am the path to the ND-JSON data file.
	* @callback I am the callback to which each record is passed.
	*/
	public void function processNdJsonFile(
		required string filePath,
		required function callback
		) {

		var file = fileOpen( filePath, "read" );

		try {

			while ( ! fileIsEoF( file ) ) {

				callback( deserializeJson( fileReadLine( file ) ) );

			}

		} finally {

			// MAKE SURE TO CLOSE THE FILE when we are done processing!
			fileClose( file );

		}

	}

</cfscript>

As you can see, we have a User-Defined Function (UDF), processNdJsonFile(), that takes a ColdFusion Closure and passes to it a deserialized ND-JSON record. The closure itself doesn't care about where that record came from or how the values are being read-in - it just processes the value. The processNdJsonFile() method, on the other hand, opens the file, reads from it, and then makes sure to call fileClose() when the processing has completed. This way, we have a very pleasing separation of concerns: the closure that processed the data doesn't care where the data came from; and, the method that manages the file doesn't care about how the data is being consumed. And, you never have to worry about forgetting to close the file.

Now, if we run this ColdFusion code, we get the following browser output:

Processing data value: 1.
Processing data value: 2.
Processing data value: 3.
Processing data value: 4.
Processing data value: 5.
Processing data value: 6.
Processing data value: 7.
Processing data value: 8.
Processing data value: 9.
Processing data value: 10.

It worked like a charm!

Using a closure to encapsulate low-level resource management has become one of my favorite techniques in Lucee CFML. As I said before, this is how I perform all Redis interactions; and, it's perfect for something like the fileReadLine() operation. It creates an excellent separation of concerns.



Reader Comments

Hi Ben. This is a great article, although I guess you have to remember to add the closure:

whenever I hear "you have to remember" in a programming context...

Only joking:)

I have just read an interesting article by Igal Sapir, who opts to use a Java Library to read each line of a file, rather than using CFML's native:

fileReadLine()

https://www.rasia.io/blog/harnessing-the-power-of-java-in-cfml.html

I am still waiting for Igal's reply to my comment, as to why he thinks this might be more efficient. Now, in all honesty, Igal never explicitly tells us this his solution is a replacement for:

fileReadLine()

After all, as Brad Wood, correctly points out, using:

createObject("Java"...)

Incurs a reflection penalty.

Reply to this Comment

@Charles,

Yeah, these days, when there's a "ColdFusion native" way of doing something, I try to use it as much as possible. Since, as you and Brad point out, the runtime probably provides an optimized approach and can make assumptions about how stuff works, rather than doing all the reflection stuff.

Reply to this Comment

Is a Coldfusion closure different to a JavaScript closure? The latter usually has an inner & outer function, like:

function makeFunc() {
  var name = 'Mozilla';
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

I cannot see how your callback conforms to this structure? To me, the callback just looks like a standard arrow function?

Reply to this Comment

@Charles,

You are correct in that the "closure" in my code isn't really taking advantage of the properties of a Closure, which is "lexical binding" - the ability to refer to values that were available in the defining context. Instead, my demo just uses the data passed to it (data). And, in that regard, you are completely correct - my "closure" could be swapped-out with a normal ColdFusion Function.

In your snippet, the powerful part of that code is that the name variable can be referenced by the Closure once the Closure is passed out of context. Imagine that my demo was fleshed-out a bit more to actually store the data somewhere, rather than just echoing it to the screen. Something like:

<cfscript>

	dataGateway = new DataGateway();

	processNdJsonFile(
		"./data.ndjson",
		( data ) => {

			// Here, the Closure is retaining a reference to the `dataGateway`
			// object, which is can consume during each line-processing.
			dataGateway.store( data );

		}
	);

</cfscript>

This now starts to look more like your example, where I'm replacing name with dataGateway if you will.

Reply to this Comment

Ben. I see where are you are going with this.

The power of your approach, is that you can pass in any level of complexity to:

processNdJsonFile()

The UDF doesn't care what the callback does, which makes it ultra flexible.

Reply to this Comment

@Charles,

1000% that! The processNdJsonFile() function only cares about reading the data-file and make sure it is closed when done (or on failure). All other responsibility is deferred to the callback, which can do anything it wants. A nice separation of concerns.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.