I'm not sure where I first heard that you could do a recursive lamdba in Ruby, but it's been simmering on the back burner of my brain for a while. I've just never had a reason to use one, until now...
I wanted to process the Rails request params, which is a hash of strings and hashes of strings and hashes of strings and hashes... you get the idea. The need was to strip all the accent marks from user input throughout the application. Here's what I came up with:
class ApplicationController < ActionController::Base
before_filter :strip_accents
protected
def strip_accents
thunk = lambda do |key,value|
case value
when String then value.remove_accents!
when Hash then value.each(&thunk)
end
end
params.each(&thunk)
end
end
That's all completely clear, right? The filter enumerates the top-level hash using the &/to_proc operator to coerce the lambda to a block for the #each
method. #each
passes the key and value to the lambda, which either removes the accents from a string, or recursively enumerates the contents of a nested hash.
I think it's totally cool that you can do this in Ruby. Everyone thinks that Ruby is just an object-oriented language, but I like to think of it as the love-child of Smalltalk and LISP (with Miss Perl as the nanny).
Totally awesome with the recursive lambda! And yeah, Ruby is just MatzLisp ;). However, I'd have to say Perl is the pervert uncle in the corner.
This is also a classic place to use the Y Combinator, avoiding the variable assignment. I just note it because this is the opportune time to figure that otherwise mysterious function.
My memory of trying to do this in Perl was that it didn't work if the assignment (and definition of the lambda) was in the same statement with the declaration of the lexical variable being assigned to. In other words: my $thunk = sub { ... &$thunk ... }; # won't compile
but: my $thunk; $thunk = sub { ... &$thunk ... }; # works
Ruby does this better (both "procedurally" and semantically.)
By passing before_filter a block you could also approach it like this:
I did something similar to this when I needed to be able to treat params like ActiveRecord objects (in that I wanted to be able to methodize the keys in order to use things like Object#try). Since params can come back as nested hashes and arrays, recursion saved my day. The snippet is on my blog here.
Josh - the problem and solution is interesting, but what I just had to comment on was your view of what Ruby is...I LOVE IT! That has got to be one of the best things I've seen you come up with (and you come up with a lot of good stuff so that's saying something)...I'm def. going to be quoting you on that the next time I get a chance. :-D
@Carl: Good alternative. I was shooting for a way to avoid having to define 2 methods, and that does it nicely too.
@Ian: does Ruby have a Y combinator? I know #returning is a good K combinator (and the #tap variant in 1.9), but hadn't seen a Y around. Not that I'd really know what to do with a Y combinator...
Archaeopteryx has had this for months. :-) Only way to get Arkx scheduling infinite streams of music without any threading/timing issues.
You can do a Y combinator in Ruby.
http://weblog.raganwald.com/2007/02/guest-blogger-tom-moertel-derives-y.html http://www.eecs.harvard.edu/~cduan/technical/ruby/ycombinator.shtml
@Josh: Another classic Y-combinator in Ruby: http://onestepback.org/index.cgi/Humor/InnovativeIdentification.red Not exactly useful, but fun.
@Adam: dead on about Perl.
'Archaeopteryx has had this for months'
Which is to say 'Practical Ruby Projects' has had this for months.
Anyway, beautiful example John. It's elegant and pragmatic.
Kids: eat your vegetables, drink your milk, don't do drugs, learn FP, and NEVER EVER put the Y-combinator in your ApplicationController.
Here's yet another take on it:
http://hamptoncatlin.com/2007/recursive-lambda-s
(including the "stack level too deep (SystemStackError)" feature) :-)
Excellent example, thanks for putting it up!
I can see using this all kinds of ways.
Nice! For everyone's enjoyment, I posted a version using the Y-combinator here:
http://mumuki2.blogspot.com/2008/06/y-combinator-for-josh-susser.html
Or you could write it like a regular old Ruby method with an optional argument that defaults to the whole params Hash:
Kaashoek - nice. No, Practical Ruby Projects didn't have it, and Archaeopteryx did.