Saturday, October 13

Ruby Revelations: An Introspection

I was code-reviewing a cohort's contribution to adPickles the other day and I came across a line of Ruby that had me completely perplexed:
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

That's quite a mouthful. If you know what that does, go ahead and stop reading now, 'cause the rest of this post is just self flagellation.

I'm no Ruby expert, but this guy is (in case you couldn't discern that from this one line masterpiece). I had to ask him to break it down for me, and after a few back-and-forths I think I've got it. Here's the skinny, for those of you that are still reading and, like me, want to see the secret unravelled.

My gut instinct is to start from the deepest nested block and work my way out, but you first need a little nugget of context.
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

An Advertisement has several aspects, such as "new", "approved", "rejected", etc. Knowing that, now we can work from the bottom up.
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

Here he's referencing a variable named advertisements which we can safely assume is an Array of objects, each being an instance of Advertisement.

He wants to call a method on the array (which in turn calls the method on each contained element) but at runtime he doesn't know the name of the method, so he constructs it on the fly.
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

In Ruby, if you want the value of a variable to be evaluated in a string, you wrap the reference in #{}, so in this case if the value of aspect is "rejected", the method name being constructed from "count_#{aspect}" is going to be count_rejected.

So each member of the Array named advertisments is going to have the method count_rejected called.
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

And for each aspect, a two-element array is created, where the first element is the value of aspect and the second element is the results returned by the on-the-fly-generated-method-name.

For example, one of these arrays might look like ['rejected',23].
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

Each of these arrays are collected into an outer containing array by the map method, which will leave us with something like [['new',14],['live',51],['rejected',23]].
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

The the whole thing gets flattened into a one-level (flat) array, like ['new',14,'live',51,'rejected',23].
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

As my cohort explains the next part, "Hash doesn't take an array as an argument, so the asterisk breaks the array into multiple arguments. Hash[*[1,2,3,4]] is the same as Hash[1,2,3,4]."
Hash[*Advertisement.aspects.map {|aspect| [aspect,advertisements.send("count_#{aspect}")]}.flatten]

Which bring us to the crusty outer shell, leaving us with a simple hash which looks like {'new' => 14,'live' => 51,'rejected' => 23}, where as you can see, each aspect is now mapped to its respective count.

Clear as mud? Excellent!

7 comments:

Anonymous said...

And Ruby is touted as a language that's more maintainable than Java due to lower LOC? This is as bad as Perl.

BTW I use Ruby and Perl extensively so I'm not biased. It just kills me when Rubyists assert the superior readability of Ruby and then write code like this.

Blake said...

While I appreciate your boy's understanding of some of the intricacies of Ruby, I think this is implemented poorly because it embraces the obtuse instead of the obvious. You can do the same thing with inject, minus the arcane syntax:

Advertisement.aspects.inject({}) {|info, aspect| info[aspect] = advertisements.send("count_#{aspect}")}

Lars said...

Code that is hard to understand is not genius, it's bad.

jux said...

Blake, I very much agree with your way of writing the code snippet, but have a minor correction. Either you need:

Advertisement.aspects.inject({}) {|info, aspect| info[aspect] =
advertisements.send("count_#{aspect}")
info
}

or

Advertisement.aspects.injecting({}) {|info, aspect| info[aspect] =
advertisements.send("count_#{aspect}")
}

using "injecting" from facets.

Brent D. said...

I'm afraid I agree with the previous comments. Clearly the guy who wrote this has a good understanding of Ruby's core methods, else he wouldn't have been able to write something like this... but that doesn't necessarily mean it's good programming practice :)

Ruby is easy to make readable. By splitting this method into multiple lines and adding some COMMENTS for crying out loud, understanding it would have been cake.

Dan Kubb said...

Every language has its own idioms, things that may not be 100% clear to people with casual understanding of a language, but are clear to someone with a good amount experience in the language. This is just one of them. I've seen this lots of times in code I've worked with so it was clear to me what it did.

However, I do agree there are other approaches (like jux's using inject) that are probably more readable. Even inject can trip people up though as the small mistake in blake's example shows.

A common idiom that would've eliminated this is to use Hash#update within the inject block, like this:

Advertisement.aspects.inject({}) {|info, aspect| info.update aspect => advertisements.send("count_#{aspect}") }

This works because Hash#update assigns to the hash AND returns the hash to be used as "info" for the next iteration of the block.

Craig said...

If all you're doing is mapping the aspects with their respective counts, what's so bad about just looping through it?

hash = {}
Advertisement.aspects.each { |aspect| hash[aspect] = advertisements.send("count_#{aspect}") }

Simple, much better than inject as far as I'm concerned.