So I have a collection of newsletters, and each newsletter has a collection of articles. Each article, in turn, has a collection of authors and a collection of categories. Now what I need to do is get a list of unique handles for all the authors and categories for a given newsletter. Here's the CFML version:
<cfset var article = "" /> <cfset var author = "" /> <cfset var category = "" /> <cfset var s = {} /> <cfloop array="#variables.newsletter.articles#" index="article"> <cfloop array="#article.authors#" index="author"> <cfset s[author.handle] = "" /> </cfloop> <cfloop array="#article.categories#" index="category"> <cfset s[category.handle] = "" /> </cfloop> </cfloop> <cfset variables.handles = structKeyArray(s) />
And here's the Groovy version:
variables.handles = variables.newsletter.articles.sum({ it.authors.collect { it.handle } + it.categories.collect { it.handle } }).unique()
I rest my case.
The use of addition ('+' operator and 'sum' method) to express appending collections together is very elegant, and coupled with 'collect' to transform each item in a collection, you get really concise and direct code. The CFML also suffers from the lack of a Set data type, so you have to fake it with the keys of a structure.
Is that something you'd really be doing in your app? Or is this kind of more of a demo of the coolness of those groovy functions?
In my CFML app it'd just be:
and done.
Granted, I use a state-of-the-art configurationless, proprietary automatic service layer and utility framework, but still. That's how my everyday, out-of-the-box CFML would look for that operation. CFMLFTW.
Oh shiz. Pwnd by the blog police. Let's try that again.
nl = app.newsletters.get( url.idNewsletter )
cfset handles = app.array( app.merge( app.unique( nl.authors, 'handle' ), app.unique( nl.categories, 'handle' ) ) )
My snippet also includes the actual retrieval of the newsletter with its articles and categories collections as queries. Woot.
Sorry, I have a distracting visitor. I totally mis-skimmed that. Real code:
arts = app.newsletters.get( url.idNewsletter ).articles
cfset handles = app.array( app.merge( app.unique( arts, 'authors.handle' ), app.unique( arts, 'categories.handle' ) ) )
The unique calls return keyed structs ( or another format if you specify ), merge melts them together, and array intelligently casts any source collection as an array ( detects input format ). Pretty fun example. 3rd try's the charm.
Yeah, that's actual code from an actual app that I actually wrote (and then committed) today at work. The difference between my examples and your example is that both of my examples are 100% of the code required and your examples require a bunch of extra custom code that you haven't shared.
No question, you can certainly synthesize Groovy-esque functionality with CFML, but my point is that you have to synthesize it, because it's not build into the language. And as your example demonstrates, you don't even get that functionality in the 'right' places; it's all packaged as utility functions on some static utility object ('app' in this case). Much nicer to have myArray.unique(), versus app.unique(myArray), both for readability and the fact that you don't need to have a reference to the all-powerful 'app' object.
For both examples, they should be prefaced with this code for newsletter retrieval. I omitted it, because it was the same for both:
The app actually runs as a module that is included into a newsletter page, so it doesn't necessarily know how to access the newsletter object based on page params. Consequently, omitting an identifier to getNewsletter() tells it to use the CMS context to retrieve the current page's newsletter.
In this particular case, the 'variables.newsletter' is a reference to a Groovy object that, among other things, provides a bunch of computed fields (like 'articles'), including lazy-loading of related entities from the JCR that is the actual data store.
PS: I added a note about the tags you can and can't use in comments, so hopefully the blog police won't get you again.
app is the shorthand I use for the application scope to accelerate readability ( thought that was obvious, sorry ), which I strategically pollute with the entire utility library as well as one instance of a magical, do-everything-imaginable object per database table ( what I call the superhero pattern ). I never even instantiate a service object, they're automatically omnipresent through ColdFusion's application scope on every request in my application, and are so tuned and refined that their startup as well as memory overhead are trivial.
The only other line of code in the entire application to get that exact functionality and for that code to work is a
in onApplicationStart. I literally have no configuration files or other setup necessary ( other than popping my decimateWorkload.cfc into the right folder on the server ).
By storing each utility method ( and each superhero object ) in the application scope, everything is centralized, inspectable, performant, and trivial to learn and remember. Whether the creators of Groovy, ColdFusion, or I wrote the underlying library and algorithms a long time ago, the same end is still accomplished. The functionality is always available, with very little code.
I realize it's a matter of taste ( and sometimes background ), but I totally ( respectfully ) disagree on your readability argument. I used to love methods as the properties of objects ( especially in my Javascript ), but I seriously prefer standalone-utility style syntax now, even without the performance benefits. It feels more natural and like more of a native extension of the language which to me at least feels very elegant and unified ( if only I could access application-scoped items implicitly, it would look just like part of the engine ).
It opens up a really great opportunity to have more flexible, powerful, logically-accessed functions. My merge function can combine any number of objects of any type. An array and a list. A struct and an array. 4 arrays. Etc. My unique method can do many different things depending on what it receives. Etc. There's always an obvious and expected behavior depending on what you pass in. I really really love being able to call a guessably-named global utility for any utility-style operation and know that it'll respond intelligently. It feels magical, while logical. And most importantly super agile.
To me having that sum method be a property of the articles collection seems weird and out of place. Not to mention it being named sum, which connotes math. Even weirder is that you're wrapping the + combination of two other method calls in what looks like object literal brackets inside the sum call ( I haven't done Groovy so I'm sure it clicks pretty quickly ). Which at a glance looks like you're passing an object literal with a single unnamed property ( the result of the call ).
It's actually really interesting to me, and I've got "check out Groovy despite its name" on my to-do list. But for my taste, at least from a fresh-to-Groovy perspective, the way that syntax plays out ( though impressively concise ) is everything but readable.
P.S. I love your blog content. You really hit a subset of CF-applicable topics that I don't see anywhere else. Props.
I totally ignored your note, sorry.
This is the only other line of code necessary for the previous code I wrote to work ( this goes in onApplicationStart ):
createObject( "component", "decimateWorkload" ).wire( "nameOfDatasource" )
Just discovered CFGroovy2, and that's simply exciting!
Can't wait to see more examples, maybe about calling coldfusion functions from groovy.
good job!
David,
I think you're missing the point. While I agree that your code and my Groovy code are roughly equal (aside from the environmental dependency of the 'app' scope), the path to using the code is very different. In order to use my Groovy code you must download Groovy, which is a community-driven and widely adopted programming language. In order to use your code you must obtain decimateWorkload.cfc.
So for you (who wrote decimateWorkload.cfc), there might not be a significant difference. However, for everyone else (who doesn't have access to decimateWorkload.cfc), there is a HUGE difference. Either they have to roll their own, or they can use a widely used and actively supported product that already has the functionality. And that is the win here.