Closures are cool, especially in ruby

Okay, a more technical programming related post.

Ruby supports closures in a really convenient way, built into the syntax. The things Ruby calls “blocks” (although you can also create a closure with ‘lambda’ keyword, and a couple other ways).

I just found a really nifty elegant use of closures to solve a problem I was having in Umlaut with configuration dependencies.

Umlaut has a whole bunch of configuration parameters. First the configuration parameters are set up with Umlaut defaults. Then certain local files are executed to allow local installations to redefine the config parameters however they want.

The trick is that some config parameters depend on others. For instance, there’s config for the name of your app, and then there’s config for the advertised name of an OpenSearch search, which uses the configged name of your app. (I’m (ab)using the AppConfig plug-in to handle my config. Just know that AppConfig::Base.key = value basically sets the key/value in a great config hash in the sky.)

   AppConfig::Base.app_name = 'Find It'
   AppConfig::Base.opensearch_short_name = "Find Journals with #{AppConfig::Base.app_name}"

Okay, this is all well and good, until we consider that whole local config over-riding global config stuff. Because maybe in your local config, you set app_name locally.  But you shouldn’t need to reset opensearch_short_name locally too, if you’re still happy with it being Find Journals with [app_name]. You might not even know opensearch_short_name exists. The problem was that the global default for opensearch_short_name was set back when app_name was something else, based on the previous value of the app name.

The solution? Closures!  Instead of setting opensearch_short_name to a string, set it to something that can take a closure defining it, and then re-run the closure after all the local config is set, re-executing the code that defined it, this time based on the new value.

We do this with a magic DependentConfig object, which takes a closure, and then, using ruby’s really dynamic language features, essentially pretends to be whatever the result of executing that closure are. Ruby makes it easy to act as a ‘proxy’ object, so DependentConfig can look just like whatever the results of that closure are, to any other code. But DependentConfig still has the closure there too (until I tell it to forget it, because we don’t need it anymore, and I’m happy to have it garbage collected in case it’s taking up a lot of memory or something. Not sure the penalty of a closure)–and since it still has the closure there, you can tell it to re-execute it and re-update it’s value at any time.  I keep a list of all DependentConfig’s that have been created in a class-variable (again, until I tell it to forget them all), so I can easily say “update all of those guys”, which I do after loading in local config.

So the code sample above turns into:

  AppConfig::Base.app_name = 'Find It'
  AppConfig::Base.opensearch_short_name = DependentConfig.new {"Find Journals with #{AppConfig::Base.app_name}"}

Nice how convenient it is in ruby to make a closure that’s an argument to a method, just put some code in {braces}, yeah?  And it’s passed as an argumetn to the DependentConfig initializer.

Then, after I’ve loaded all local config, and want to recalculate any config that depends on values that may have changed, I just call:

    DependentConfig.permanently_reset_all

I’m rather too pleased with myself for this elegant solution. If anyone can think of any pitfalls here, do let me know.  You can look at the code for the DependentConfig proxy class in my svn. (It’s not in trunk yet, it’s part of my branch moving Umlaut to rails2, which should be back in trunk soonish.)  Only 50 fairly sparse and elegant (and, I think, relatively easy to understand, tell me if I’m wrong) lines of code, not counting the initial introductory comments, and including lots of whitespace lines and copious internal comments (I’m a verbose documenter).

update: duck typing bites back, trouble in river city

I’m afraid that when I pass a DependentConfig object that is proxying a string to Rails render, because the string identifies the name of a template to render, it makes render all confused, I think it thinks I’m passing it an already instantiated template object of some kind, instead of a string identifying a template. Bah! When duck typing and flexible arguments bites back.

The fix is simply calling:

render :partial => variable.to_s

instead of just:

render :partial => variable

When variable is a DependentConfig masquerading as a String. But I’m no longer so pleased in the complete transparency of my DependentConfig proxy. Oh well. Still worth it, I think.

More update: Oops, I just discovered that the ruby standard library already has a class to help you create this kind of a proxy/delegator object. http://ruby-doc.org/stdlib/libdoc/delegate/rdoc/index.html  I bet once I restructure my class to use that, all of my problems with imperfectly impersonating the result object will go away. Use the wheel that’s already been tested by others, not try to reinvent my own! That’ll make my class like 10 lines instead. Will update here when I have that done.

update 20 Nov: Okay, the code in svn is now based on the standard library Delegator class, providing much more complete impersonation effect. Sadly, the #to_s call when using it as a string argument to a Rails render method is still neccesary. This is annoying. I blame poor coding practices in Rails render (could it be actually looking at the specific #class of it’s argument? That’s not playing well with duck typing), but don’t have time to get to the bottom of it.  More thoughts on coding practices, ruby, and design patterns sparked by this design can be found here.

This entry was posted in General. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s