Base on the suggestion from
Rob Wilkerson, I decided to pull
this out into some code that illustrated the point I made in my last post.
For context, see
Another great use for closures.
Basically, what's going on is that we have a
list()
method that takes a query and
dynamically outputs a list of its rows (with a heading column as well). It also has the ability to
call a closure for a cell instead of doing its normal output, if one exists.
Most of the code below is tangential to the example, but as a whole, it illustrates the point I was trying to
make, and shows a practical aspect of closures (using Sean Corfield's
Closures for CFMX).
In particular, there are a couple of methods of interest. Rather than passing a closure directly into the method, as one
might normally do, I created a function,
onOutputOf()
, that puts the closures into a data
structure to be checked by the method. This is only because it made more
sense in my original application to do it that way, since the programmers are not typically calling the
list()
function themselves.
Next, there is a method to find a closure for a column if it exists. Rather than have the list function
checking the closure storage data structure itself, I opted to use
getClosureToBeRunOnColumn()
to
keep the methods as cohesive as I could see.
Of course, the
list()
method itself shows the use I found for this. And finally,
callClosure()
shows how I'm using Closures for CFMX, and the next to last line shows an example of passing in some code as a string.
If you have any questions (or comments on how I can improve this!) please feel free to let me know. I enjoy the
feedback =). I'm done rambling now. Here's the code:
<!--- a simple utility function to make this example work --->
<cffunction name="queryRowToStruct"
>
<cfargument name="query"
>
<cfargument name="rowNum"
>
<cfset var local=structNew()>
<cfset local.result = structNew()>
<cfloop query="arguments.query"
>
<cfloop list="#arguments.query.columnList#"
index="local.col"
>
<cfset local.result[local.col] = arguments.query[local.col][arguments.rowNum]>
</cfloop>
</cfloop>
<cfreturn local.result>
</cffunction>
<!--- set up a query for the example to use --->
<cfset beers = queryNew("beer,in_stock,rating"
,"varchar,bit,integer"
)>
<cfset queryAddRow(beers,3
)>
<cfset querySetCell(beers,"beer"
,"Boddington's"
, 1
)>
<cfset querySetCell(beers,"in_stock"
, true, 1
)>
<cfset querySetCell(beers,"rating"
, 8
, 1
)>
<cfset querySetCell(beers,"beer"
,"Newcastle"
, 2
)>
<cfset querySetCell(beers,"in_stock"
, false, 2
)>
<cfset querySetCell(beers,"rating"
, 9
, 2
)>
<cfset querySetCell(beers,"beer"
,"Milwaukee's Beast"
, 3
)>
<cfset querySetCell(beers,"in_stock"
, true, 3
)>
<cfset querySetCell(beers,"rating"
, 1
, 3
)>
<cfset closuresToRunOnColumns = arrayNew(1
)>
<!---
this function takes all the closures to run on columns
and stores them for later use.
--->
<cffunction name="onOutputOf"
output="false"
>
<cfargument name="column"
>
<cfargument name="run"
>
<cfset var singleClosureToRunOnColumn = structNew()>
<cfset singleClosureToRunOnColumn.column = column>
<cfset singleClosureToRunOnColumn.closure = run>
<cfset arrayAppend(closuresToRunOnColumns, singleClosureToRunOnColumn)>
</cffunction>
<!---
given a column name, this function returns the
closure if it exists, ""
otherwise
--->
<cffunction name="getClosureToBeRunOnColumn"
>
<cfargument name="column"
>
<cfset var local = structNew()>
<cfset local.result = ""
>
<cfloop from="1"
to="#arrayLen(closuresToRunOnColumns)#"
index="local.i"
>
<cfif closuresToRunOnColumns[local.i].column eq column>
<cfset local.result = closuresToRunOnColumns[local.i].closure>
<cfbreak>
</cfif>
</cfloop>
<cfreturn local.result>
</cffunction>
<!---
this function creates and calls the closure so it is easy
for the programmer to just inline his closures
--->
<cffunction name="callClosure"
>
<cfargument name="closure_code"
>
<cfargument name="row"
>
<cfscript>
closure_factory = createObject("component"
,"org.corfield.closure.ClosureFactory"
);
closure = closure_factory.new(closure_code).bind(outer=row);
closure.call();
</cfscript>
</cffunction>
<!--- this is our generic list function --->
<cffunction name="list"
output="true"
>
<cfargument name="query"
required="true"
>
<cfset var local = structNew()>
<cfset local.columns = query.columnlist>
<cfloop list = "#local.columns#"
index="local.col"
>
#local.col#,
</cfloop>
<br/>
<cfloop query="arguments.query"
>
<cfloop list="#local.columns#"
index="local.col"
>
<cfsavecontent variable="toBeOutput"
>
<cfset local.closureToCall = getClosureToBeRunOnColumn(local.col)>
<cfif local.closureToCall is ""
>
<!--- if the column does not have a closure to run, output it --->
#arguments.query[local.col][currentRow]#
<cfelse>
<!--- otherwise, call the closure --->
<cfset callClosure(local.closureToCall,queryRowToStruct(arguments.query,currentRow))>
</cfif>
</cfsavecontent>
#trim(toBeOutput)#,
</cfloop>
<br/>
</cfloop>
</cffunction>
<!---
Everything above this is hidden in our base class.
Everything below is the stuff the end programmer
needs to deal with.
--->
<!--- here's how the normal list looks --->
<cfset list(beers)>
<br/>
<!---
But when the programmer calls it, he wants to change
it. Instead of outputting 0/1 for in_stock, he wants
it to say "yes"
or "no"
Also, he knows by documented convention that whatever variables
are sent to his closure, they will be in a structure called
"arguments"
--->
<cfset onOutputOf(column="in_stock"
,run="<cfoutput>
##yesNoFormat(outer.in_stock)##</cfoutput>
"
)>
<cfset list(beers)>
Hey! Why don't you make your life easier and subscribe to the full post
or short blurb RSS feed? I'm so confident you'll love my smelly pasta plate
wisdom that I'm offering a no-strings-attached, lifetime money back guarantee!
Leave a comment
Slight fix to one of the comments: anything passed into the closure is not in the "arguments" scope in the code above. I've placed it into the "outer" scope instead. So the comment that says:
Also, he knows by documented convention that whatever variables are sent to his closure, they will be in a structure called "arguments"
should instead read:
Also, he knows by documented convention that whatever variables are sent to his closure, they will be in a structure called "outer"
Posted by
Sam
on Apr 28, 2007 at 03:34 PM UTC - 6 hrs
Not sure why you're using .bind() here - why not pass the row as an argument to .call() instead? .bind() is when you want to bind in a variable after creating the closure but then you pass the closure around and call it elsewhere (with arguments).
Also, in callClosure() you are creating a new factory and a new closure object on every row. You would do better to create the factory once for your application and then just create a closure instance once for each onOutputOf() call and store that in a struct (accessed by column name - you don't need an array: closuresToRunOnColumn[colName] = theClosure - then you don't need to loop through the array to retrieve a closure, just dereference the struct).
Posted by
Sean Corfield
on Apr 28, 2007 at 11:00 PM UTC - 6 hrs
Sean, believe it or not there is a good answer (not!) to your questions (well, sort of).
The short answer is I pulled this out of a real app trying to make it somewhat sensible without the rest of it, and didn't pay as much attention to detail as I should have. So, you're advice is good. (but, you knew that) =)
The longer explanation follows:
1) I was using bind() because (in the original application) I wanted to be able to make changes (in general, not just in the list function) to the outer struct and have them cascade outside the closure. I think that's the right call for it, but I welcome corrections (of course)
2) About instantiating the factory for each closure. I never thought about the repercussions in the example. Clearly, as I've shown it in the example is not the right thing to do. I never even noticed when I put it inside there (in fact, in the real code, the callClosure() method looks more like closure_factory.new(closure_code).bind(outer=row).call()
(not looking at it, just going from memory)
3) Finally, using the array as a data structure to hold this in was just retarded on my part. It was some combination of "I've been using arrays to store my stacks of functions to run" (because they are called before the object is fully instantiated) and "oh, linear time to find out if there is a closure to call or not is good enough."
You'll be glad to know that earlier today I already migrated away from that and used the constant time hashes as you suggested.
Thanks for the insights as well. I need them =)
Posted by
Sam
on Apr 28, 2007 at 11:43 PM UTC - 6 hrs
Re: 1. and 2. - .bind(name=object).call() is essentially the same as .call(name=object) so you never need .bind() if you are calling the closure in the *same* context.
You only need .bind() if you are binding variables in one context and calling them in another context.
The whole .bind(variable_bindings) / .call(argument_bindings) thing is what gives closures their power but it's also the hardest part to "get".
Posted by
Sean Corfield
on Apr 29, 2007 at 06:46 AM UTC - 6 hrs
You were up early (or late?) =)
Thanks for that last clarification. I see your point now, so I'll be changing that to pass in via an argument to the call method instead of the bind method.
Really, it does make more sense that way I think.
Posted by
Sam
on Apr 29, 2007 at 09:30 AM UTC - 6 hrs
Leave a comment