11 September, 2013
This kind of thing makes me twitch:
def sold_widgets sold_widgets =  for widget in @widgets sold_widgets << widget if widget.sold? end sold_widgets end
If you’ve been a Ruby programmer for long, you’ll be twitching too. How many things you’re twitching at (there’s a lot) probably determines how long you’ve been coding Ruby, and roughly how much of the Ruby Way kool-aid you’ve consumed.
Today, I want to look at a couple of things. The first that most people will spot (since it’s one of the first things most Rubyists seem to pick up) is the use of a
for loop. No no no no no, we say. We know our
Enumerable mix-in, so let’s use an iterator and pass it a block:
def sold_widgets sold_widgets =  @widgets.each do |widget| sold_widgets << widget if widget.sold? end sold_widgets end
Now, that looks a bit better, but we haven’t really changed much. Yes, it’s more “idiomatic”, but visually, we really only changed the syntax used to extract the
widget from our
Enumerable lets us do better.
If you’re like me, the whole “set up container variable, populate container variable, return container variable” has always felt very “un-Ruby”. In this case, we can dispense with the local
sold_widgets variable altogether by using another
def sold_widgets @widgets.select do |widget| widget.sold? end end
To anyone calling
sold_widgets now, nothing has changed, but internally we’ve got rid of a lot of the boilerplate, as well as the hanging, implicit return of the local
sold_widgets—which always feels like a hint that we’re doing something un-idiomatic.
Now this is great: we’ve dropped our five liner down to what could readably be turned into a one-liner. If we don’t mind using the symbol-to-proc pattern, we can even drop it to this:
def sold_widgets @widgets.select &:sold? end
which I’d argue loses none of the clarity of what we are doing. If anything, it heightens it.
Now, let’s consider something similar but different:
def description description = "This is a widget. " description << “It has already been sold. “ if sold? description << “It is a limited edition. “ if limited? description end
This gives me a similar sense of twitchiness. We’re not iterating this time, but we still have the “set up container variable, populate container variable, return container variable” pattern. The repetition and the implicit return at the end are the big smells here.
Wonderfully, Ruby 1.9 has our back here: though we’re not mixing in
Enumerable, we do have an all-purpose way to work with any arbitrary Ruby object “in place”, just like using
def description "This is a widget. ".tap do |desc| desc << “It has already been sold. “ if sold? desc << “It is a limited edition. “ if limited? end end
No, it doesn’t reduce the number of lines of code, but it does drop our method to a single expression, the result of which is returned directly (just like our
select example above).
tap allows any Ruby object to accept a block, with itself (not a reference) passed into the block for manipulation before returning itself. It’s amazingly handy for chaining modifications, or for performing conditional manipulations on an object before returning it. It allows us to (mostly) abandon the “container, populate, return” pattern in favour for a much more Ruby-ish “return manipulated object” pattern.
In fact, this sort of method chaining is an example of a Fluent Interface, which sounds like a very Ruby-ish pattern to me.
Now I probably overuse
tap. I've seen it cause any number of seasoned Rubyists to scratch their heads when they first see it, which probably means it can make code harder to change in some teams (remember: house style trumps “idiom” every single time). This isn’t intended as some militant call to arms.
However, I do feel like we were in the same position with
for…in many years ago: the arguments of “readability” and “intuition” back then were more about “familiarity” than anything intrinsicly better in the syntax. Personally, I find that
tap feels more like Ruby, and given Ruby’s propensity for optimising for programmer happiness, I think the “feel” is a valid argument for its usage.
That said, I suspect using
tap makes me a little too happy sometimes. I should really get out more.
tap? Hate it? Think my examples are dumb? Shout at me on twitter.