a journal
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 widgets
. 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 Enumerator
method:
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 Enumerable#each
, Enumerable#select
, Enumerable#collect
etc.
Enter Object#tap
:
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.
Love tap
? Hate it? Think my examples are dumb? Shout at me on twitter.