Using CFML Files To Pre-Process CFM Files In ColdFusion
A lot of the cool stuff that I've learned in Seven Languages in Seven Weeks depends on the ability to write lambda / anonymous functions in your code. As of version 9.0.1, ColdFusion still doesn't allow for anonymous functions. But, it does allow for onMissingTemplate() functionality. As a fun little experiment, I wanted to see if I could use the onMissingTemplate() event handler to generate CFM files from pre-processed CFML files that made use of anonymous functions.
The idea is simple, I want to write code that allows for functions to be defined as arguments of other functions. You can already do this by pre-defining your argument-functions first and then passing them in as references. But, I wanted to be able to define my argument-functions as part of the higher-order function:
test.cfml (NOTE: CFML File)
<!--- I apply the given function to the given argument. --->
<cffunction name="applyFunction">
<cfargument name="function" />
<cfargument name="argument" />
<!--- Apply function, return result. --->
<cfreturn arguments.function( arguments.argument ) />
</cffunction>
<!--- Use an inline function to double the input. --->
<cfset result = applyFunction(
<function( in ){
<cfreturn( in * 2 ) />
}>,
3
) />
<!--- Output results. --->
<cfoutput>
Double Result: #result#
</cfoutput>
As you can see, I have a higher-order function (a function that takes other functions as arguments). This higher-order function - applyFunction() - applies a given function to a given argument. It's a contrived example, but it gets the point across. When I invoke that function, I am defining the function argument inline with the rest of the parameters.
Now, of course, this CFML file won't parse properly, so we can't execute it directly. What we can do, however, is call a corresponding CFM file and perform the pre-processing of the CFML file within the onMissingTemplate() event handler.
Application.cfc
<cfcomponent
output="false"
hint="I define the application settings and event handlers.">
<!--- Define the application settings. --->
<cfset this.name = hash( getCurrentTemplatePath() ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) />
<cffunction
name="onMissingTemplate"
access="public"
returntype="boolean"
output="true"
hint="I handle missing templates, and pre-process CFML files when necessary.">
<!--- Define arguments. --->
<cfargument
name="script"
type="string"
required="true"
hint="I am the missing CFM file that was requested."
/>
<!--- Define the local scope. --->
<cfset var local = {} />
<!---
Since we are dealing with pre-processing CFML files, we
want to single-thread this aspect of the page request (an
outlier case) to make sure concurrent requests don't try
to process the file at the same time.
--->
<cflock
name="#hash( arguments.script )#"
type="exclusive"
timeout="30">
<!---
Check to see if a CFML version of the page exists. If
so, we will pre-process it.
--->
<cfif fileExists( expandPath( arguments.script ) & "l" )>
<!---
A request for a CFM file came through that
corresponds to a CFML file that has yet to be
pre-processed. Convert the CFML file to a CFM
file and execute it.
--->
<cfset this.processCFML( arguments.script & "l" ) />
<!--- Include the resultant request. --->
<cfinclude template="#arguments.script#" />
<!---
Return true to indicate that the missing template
has been properly handled.
--->
<cfreturn true />
</cfif>
</cflock>
<!---
If we made it this far, we could not pre-process any
file. Return false to indicate that the template was
not handled properly.
--->
<cfreturn false />
</cffunction>
<cffunction
name="processCFML"
access="public"
returntype="void"
output="false"
hint="I process the given CFML page, turning it into a CFM page at the same address.">
<!--- Define arguments. --->
<cfargument
name="cfmlScript"
type="string"
required="true"
hint="I am the CFML script that we need to convert to a CFM page."
/>
<!--- Define the local scope. --->
<cfset var local = {} />
<!--- Convert the CFML script to a file path. --->
<cfset local.cfmlFilePath = expandPath(
arguments.cfmlScript
) />
<!--- Create the CFM script file path (chop off the L). --->
<cfset local.cfmFilePath = reReplace(
local.cfmlFilePath,
"l$",
"",
"one"
) />
<!--- Read in the CFML file. --->
<cfset local.cfmlCode = fileRead( local.cfmlFilePath ) />
<!---
Create an array to hold any data that we need to prepend
to the resultant code file.
--->
<cfset local.prependCode = [] />
<!---
Create a string buffer to hold our CFM code that is
being processed.
--->
<cfset local.buffer = createObject(
"java",
"java.lang.StringBuffer"
).init()
/>
<!---
Create a pattern to match our possible pre-processing
elements. For now, the only we are going to be catching
is the inline function processing.
--->
<cfsavecontent variable="local.regex">(?xi)
(?:
< function\(
( [^)]* )
\)\{
(
((?!\}>)[\w\W])+
)
\}>
)
</cfsavecontent>
<!--- Compile the pattern. --->
<cfset local.pattern = createObject( "java", "java.util.regex.Pattern" ).compile(
javaCast( "string", local.regex )
) />
<!--- Get the matcher for our pattern. --->
<cfset local.matcher = local.pattern.matcher(
javaCast( "string", fileRead( local.cfmlFilePath ) )
) />
<!--- Keep looping over matches. --->
<cfloop condition="local.matcher.find()">
<!---
We found an inline function. We now have to convert
it to to a local UDF. Create the name of the UDF.
--->
<cfset local.udfName = "udf_#hash( createUUID() )#" />
<!--- Create the open Function. --->
<cfset arrayAppend(
local.prependCode,
"<cffunction name='#local.udfName#' output='false'>"
) />
<!--- Create an argument for each argument. --->
<cfset arrayAppend(
local.prependCode,
(
"<cfargument name='" &
reReplace(
trim(
local.matcher.group( javaCast( "int", 1 ) )
),
"\s*,\s*",
"' /> <cfargument name='",
"all"
) &
"' />"
)
) />
<!--- Append the function body. --->
<cfset arrayAppend(
local.prependCode,
local.matcher.group( javaCast( "int", 2 ) )
) />
<!--- Create the close of the function. --->
<cfset arrayAppend(
local.prependCode,
"</cffunction>"
) />
<!---
Replace the inline function with the UDF reference
that will be prepended to the code file.
--->
<cfset local.matcher.appendReplacement(
local.buffer,
local.udfName
) />
</cfloop>
<!---
Append any of the file that did not need to be
pre-processed.
--->
<cfset local.matcher.appendTail( local.buffer ) />
<!--- Flatten the prepend code and the buffer code. --->
<cfset local.cfmCode = (
arrayToList(
local.prependCode,
(chr( 13 ) & chr( 10 ))
) &
local.buffer.toString()
) />
<!--- Write the pre-processed CFM code to disk. --->
<cfset fileWrite( local.cfmFilePath, local.cfmCode ) />
<!--- Return out. --->
<cfreturn />
</cffunction>
</cfcomponent>
This code is kind of complicated, so I'll break down what it does. When the user requests a CFM file that doesn't exist, onMissingTemplate() checks to see if there is a CFML file at the same address. If so, it pre-processes that CFML file and then writes the resultant CFM file to disk. It then includes the newly generated CFM file. As part of the pre-processing, we are simply parsing out all of the inline function definitions:
<function(){ ... }>
... and replacing them with actual ColdFusion user-defined functions (UDFs), which we prepend to the resultant CFM code file.
Now, of course, the most serious limitation of this approach is that it can only be applied to top-level scripts. Using the onMissingTemplate() approach, you wouldn't be able to use this with any included file. But, this was just a fun experiment. And, when we request "test.cfm", we get the following output:
Double Result: 6
As you can see, the applyFunction(), was able to double the input using the inline, lambda function.
If you are curious, this is the CFM file that gets produced from the CFML pre-processing:
NOTE: I have added some white-space for readability.
test.cfm (NOTE: CFM File)
<cffunction
name="udf_24C8E39A1A5D8AB1B0A4685108ECCB15"
output="false">
<cfargument name="in" />
<cfreturn( in * 2 ) />
</cffunction>
<!--- I apply the given function to the given argument. --->
<cffunction name="applyFunction">
<cfargument name="function" />
<cfargument name="argument" />
<!--- Apply function, return result. --->
<cfreturn arguments.function( arguments.argument ) />
</cffunction>
<!--- Use an inline function to double the input. --->
<cfset result = applyFunction(
udf_24C8E39A1A5D8AB1B0A4685108ECCB15,
3
) />
<!--- Output results. --->
<cfoutput>
Double Result: #result#
</cfoutput>
As you can see, our inline function definition was simply replaced with a UDF reference which was prepended to the resultant code file.
This was just an experiment - something that I wanted to try in order to apply some of the features that I've learned from Seven Languages in Seven Weeks. I wouldn't suggest putting anything like this into production. I know a lot of people have hoped for lambda functions in ColdFusion for a long time; me too. I think they will really take the language to the next level!
Want to use code from this post? Check out the license.
Reader Comments
You may have run across this in your book, but after reading through this post the question in my mind regarding lamda functions is "why"?
I cant really see much of a benefit. Does it allow me to do something I cant already elegantly do without them? Is there some performance benefit? I'm sure there must be some other reason other than "all the cool languages have it", and perhaps someone can chime in with an example that makes a strong case for adding lamda to CFML?
I'm also wondering why...
I'm not sure what the benefit is of passing in a function anonymously vs defining it with a meaningful name and passing in that variable.
I would probably settle for adding the function type for the argument definitions so you can at least define an argument as expecting a function as an argument. Using "any" or "struct" doesn't seem correct (I'm not actually sure if "struct" is accepted for functions, but I know it's accepted for components, so it's an assumption on my part).
Great Post Ben.
I know this is only a proof of concept, but what about storing the new anonymous function in a struct at the application, page or some other cache level Scope.
I'm on board with with your idea you use a special pre-processor syntax as you it needed to achieve the complete function definition. To keep function definition more CF native you could just use cffunction to define a function (the name is irrelevant ), then use a similar construct of applyFunction. the first argument is the function the second argument could be a struct / array of actual argument. Like a ruby block but defining the function.
Rather then doing with at the application level a function could be including at the page level.
I'm excited to see this concept develop b/c I see it as a missing feature to CF. Especially if you want to some sort of "evented" programing, using cf.
Think re-implementing Ruby's Sinatra in cfml or an entire web app with only Application.cfc
@Peter is this what you were thinking?
http://pastebin.com/sENPFkJP
I bet Ben has already blogged about passing functions as arguments like that though, so he had to try to out-genius himself with this post.
I agree it would be cool to have native support for lamda. ::sigh:: Someday...
@Anthony, @Mike,
There are really two issues at play here, only one of which would currently apply to ColdFusion. The first, universal concept, is that of convenience. Yes, you can predefine the functions and then pass them in; but, being able to define them inline just adds a level of convenience. After all, most "syntactic sugar" is simply a form of convenience.
But, convenience is a good thing. Take the "++" operator. While we can very easily get by without it, once ColdFusion added it, I started using it immediately - it's just easier.
As far as a "meaningful name" goes, I guess the point is that it doesn't really have a meaning name. Or rather, the name wouldn't add any value. Remember, these are one-off functions - part of why defining them inline is so appealing. Their meaning is defined by their context.
Think about Javascript and jQuery; we very often see people bind event handlers like this:
Here, we don't need to name the function because we know exactly what it does by its context (form submission event binding). Sure, we could predefine the function as something like "submitHandler"; but, I would guess that it wouldn't add a significant amount of value.
If this looks "right" in Javascript and "wrong" in ColdFusion, it's probably just because the language doesn't allow for it and hence, it is not used. If the language allowed for it, I am sure it would start be a more obvious choice.
Also, I said there were two points at play here; the latter point would be about closures. This really has nothing to do with the syntax, but the type of scope chain binding that is used in closures is typically associated with languages that can perform inline function definitions (or code blocks, etc.).
@Peter,
Yeah, as Russ pointed out, you can definitely store functions in any scope you want. The one thing that you can't do it define the scope path in the function tag:
That will throw the following error:
Using the pre-processing, however, you could do something like this without a problem:
Since the pre-processor just does strait up substitution, this would work just fine (which I think is what you were getting at).
@Ben
Does this type of problem make you want to work in other languages that have more syntactic sugar?
[ ] yes
[ ] no
[ ] maybe
@Russ,
There's no doubt that other languages have some syntactic sugar. But, at the same time, ColdFusion has so much native power. I wouldn't want to lose all that.