There are plenty of uses for closures, but two of the most useful ones I've found
(in general) are when you really want/need to encapsulate something, and when you want to implement
the
Template Method pattern.
Actually, implementing the Template Method pattern using closures may be misusing it, or
I may be mischaracterizing it. It's more of like a "generalized" template
method (particularly since we're not following the pattern, but implementing the intent).
I still don't understand all of the Gang of Four patterns, so have mercy on me if I've
gotten it wrong. =) More on all this below.
So as I get ideas from other languages, I always wonder "how can I use that idea in language X."
For instance, I like the way Coldfusion handles queries as a data structure - so I tried to
implement something similar in Java (which, I
will eventually get around to posting).
I encountered closures for the first time in Lisp a few years ago in an Artificial Intelligence
course, I'm sure. But it wasn't until more recently in my experience with
Ruby that I started to understand them. Naturally, I thought "could this be done in Coldfusion?"
Luckily for us,
Sean Corfield made a Closures for CF
package available a couple of months ago (as far as I can tell).
A couple of days ago, I gave it a whirl.
The first thing I have to say is that I had to move the CFCs into the same
directory as my
closure_test.cfm
file because it wasn't able to find the
temporary templates it was generating. Sean let me know it was because of a pre CF 7.0 bug in
expandPath()
, which explains why I had trouble with that function in cfrails. Anyway,
I've posted a "fix" for 6.0/6.1 to the comments on his blog, which basically just
replaces
expandPath(".")
when it is creating a temp file to
use
getDirectoryFromPath(getCurrentTemplatePath())
.
After that, it did take a while to understand what was going on, but once I figured it out,
it was pretty cool. So here's my first class of using closures, and what I did in Coldfusion
to implement it: really "encapsulating" something.
Encapsulation is a word you see thrown around quite often. But you don't achieve it by
putting getters and setters everywhere - particularly if you are passing back complex data
structures from a
get()
method (without first making duplicates, of course).
The idea behind it, as far as I've been able to understand, is to hide implementation
details from code which doesn't need to know it. Not only does this make the client
code need to be less complex, it certainly lowers your coupling as well. However,
if you have a
Container
(for instance), how can you encapsulate it?
If you return an array representation, you've just let the client code know too much.
If you don't let the client code know too much, how will it ever iterate over the
contents of your
Container
?
Certainly an
Iterator
comes to mind. You could provide that if you wanted to,
complete with
next()
,
previous()
and anything else you might need.
But you could also use a closure to allow you to encapsulate iteration over the
Container
,
while still allowing you the freedom to do what you wanted within the loop. Here's my CF code:
<!--- container.cfc --->
<cfcomponent>
<cfscript>
variables._arr = arrayNew(1
);
variables._curIndex = 0;
// adds an element to the container
function add(value)
{
_curIndex = _curIndex + 1;
_arr[_curIndex]=value;
}
// iterates over the container, letting a closure
// specify what to do at each iteration
function each(closure)
{
closure.name("run"
);
for (i=1; i lte _curIndex; i=i+1)
{
closure = closure.bind(value=_arr[i]);
closure.run();
}
}
</cfscript>
</cfcomponent>
And here's the resulting code that uses
Container.each()
, and passes a closure to it:
<!--- closure_test.cfm --->
<cfscript>
cf = createObject("component"
,"org.corfield.closure.ClosureFactory"
);
container = createObject("component"
,"container"
);
container.add(10
);
container.add(20
);
container.add(30
);
beenhere = false;
c = cf.new("<cfset value = value + 3>
<cfoutput>
#value#</cfoutput>
<cfset beenhere = true>
"
);
container.each(c);
c = cf.new("<br/>
<cfoutput>
This container has the value #value# in it</cfoutput>
"
);
container.each(c);
</cfscript>
<cfoutput>
#beenhere# <!--- outputs false --->
</cfoutput>
Now, from my understanding of closures, you should be able to modify "outer" variables within them. Thus,
to be a "true" closure, outputting
beenhere
above
should show true. This
would be relatively easy to do by sending the appropriate scopes to the closure,
but the only ways I can think of to do that would muck up the syntax. Other than that, it is not code
that makes me comfortable, so to speak. The
closure_test
needs to know that it is expected
to use
value
as the name of the variable. There may very well be a simple way around this that
doesn't screw up the syntax, but I have yet to spend enough research in figuring it out. In fact, in
Ruby you'd use
|value| do_something_with(value)
, so it's probably as simple as that, or providing
an extra parameter (as I saw in one of Sean's examples). Perhaps I will
when I run across a need for it (and I'm sure I will be using this in the future).
And now for comparison purposes, I've included the Ruby version that does the same thing:
class Container
def initialize
@arr = []
@curIndex=-1
end
def add(value)
@curIndex = @curIndex+1
@arr[@curIndex] = value
end
def each
cnt = 0
(@curIndex+1).times do
yield @arr[cnt]
cnt += 1
end
end
end
#trial run
container = Container.new
container.add 15
container.add 25
container.add 35
beenhere = false
container.each{|v| v+=1; beenhere=true}
puts beenhere #displays true
This isn't normally how you'd code in Ruby, but I tried to keep the look approximately the same.
In particular, the
each()
method is already implemented for arrays in Ruby, so there
would certainly be no need to rewrite it.
As you can see, the syntax is a bit cleaner in Ruby than in the CF version. It's so easy to do,
one doesn't need to think about using closures - you just do it when it comes naturally. Even with
all that, I think Sean reached his goal of proving himself wrong (he said
"I was having a discussion about closures with someone recently and
I said they wouldn't make sense for ColdFusion because the syntax would be too ugly.
On Friday evening, I decided to try to prove myself wrong"). I think it might be possible to
improve it a
little, but I don't think it would ever become as "pretty" as Ruby's style (unless, of
course, it was built into the language). Because of that, and the effort involved in creating them,
I don't think it will ever become a natural solution we turn to in CF (although, if I started using them
enough, it would eventually become a natural solution). In any case, he certainly did an awesome job,
made the syntax much cleaner than I would have thought possible, and gave us a new tool to solve problems with.
In closing, I'd like to talk a bit about the "other" use for closures I've found, what I called a generalized
Template Method pattern above. Really, this is just a general case of the way encapsulation was furthered
with the
each
method above. Basically, you are defining the skeleton of an algorithm, and allowing
someone else to define its behavior. I mentioned that it's not really the template method pattern,
since we aren't confining it to subclasses, and we aren't actually following the pattern - we're just implementing
its intent, in a more general way. So basically, you might think of it as a delayed evaluate for letting
some other code customize the behavior of your code that expects a closure. I don't know if I've made
any sense, but I sure hope so (and if not, let me know what I might clarify!).
Finally I'd like to thank Sean for providing this tool to us, and ask everyone, "what uses
have you found for closures, if any?"
Update: Thanks to
Brian Rinaldi, Sean Corfield found this post and
addressed the problems I encountered. It turns out I was just doing it wrong, so to see it done right, visit his blog. I also responded to a few of the issues raised
here.
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!