Matt Briggs

"Not all code needs to be a factory, some of it can just be origami" -why, the lucky stiff

Why I Like Object#tap

| Comments

In a recent Destroy All Software screencast, Gary mentioned how he really doesn’t like Object#tap. He was using it in this sort of context

1
2
3
4
5
6
7
8
9
10
class StoreCache
  def self.for_term(term)
  begin
    CachedScore.for_term(term)
  rescue CachedScore::NoScore
    RockScore.for_term(term).tap do |score|
      CachedScore.save_score(term, score)
    end
  end
end

He said he didn’t understand why people like that syntax so much, when you could just as easily do

1
2
3
4
5
6
7
8
9
10
class StoreCache
  def self.for_term(term)
  begin
    CachedScore.for_term(term)
  rescue CachedScore::NoScore
    score = RockScore.for_term(term)
    CachedScore.save_score(term, score)
    score
  end
end

with the differences being that the name of the variable is on the left side, and the return is more explicit. I sort of get where he is coming from, but I would not use tap that way.

What Object#tap means to me

I think he (and many others) see Object#tap as meaning “fancy method that give me a 1 character placeholder variable and implicit return”. I see tap as meaning “tap into the object initialization”, or more practically “This entire expression is related to object initialization.”

Typically, I wont use tap unless there is a high degree of locality, and you are talking about left-side = right-side type code. Something like this

1
2
3
4
5
6
def build_foo
  Foo.new do |f|
    f.bar = "Hi"
    f.baz = "Baz"
  end
end

Building out values on an object is an incredibly common pattern that is logically a single thing. Visually, tap is grouping the code for that pattern. Also, I find it reduces density in a place where the additional verbosity really doesn’t add anything in terms of clarity. At work, we are still using 1.8.7 ree, so when we need ordered hashes (often as identifiers for keys on objects), we have a lot of code that looks like this

1
2
3
4
UNIT_OF_MEASURES = ActiveSupport::OrderedHash
UNIT_OF_MEASURES[1] = "Eaches"
UNIT_OF_MEASURES[2] = "Cases"
UNIT_OF_MEASURES[3] = "Pallets"

I think the move from that to tap style is a significant improvement

1
2
3
4
5
UNIT_OF_MEASURES = ActiveSupport::OrderedHash.tap |uom|
  uom[1] = "Eaches"
  uom[2] = "Cases"
  uom[3] = "Pallets"
end

The last thing is the fact that its a single expression. I love implicit returns in ruby where your entire method is a single expression, it feels kind of lispy. Something like this

me likey
1
2
3
def foo
  some_predicate? ? "Hi!" : "Bye"
end

However, I am really not a fan of implicit returns when you just end a function with a bare word. If you are writing imperative style of code, I think each statement should actually be a statement that says what it does. Something like this just sort of feels like a mis-use of a language feature.

ugh
1
2
3
4
5
def foo
  thing = build_thing
  thing.some_method
  thing
end

This is something that I think falls squarely into personal style. But because of how I enjoy writing more expression oriented code, having an expression for a common pattern is a big plus for me.

Another interesting thing to note is that in rails-land, it is very common to use hash initializers for this kind of thing. Something like this

1
2
3
Post.create! author: current_user,
             published: true,
             category: "some-category"

While that syntax is very minimal, I actually prefer the Object#tap style of api, because I find it gives a clearer separation between plain old method arguments, and object initialization.

1
2
3
4
5
Post.create! do |p|
  p.author = current_user
  p.published = true
  p.category = "some-category"
end

Not Hatin On Gary

The dude is awesome, and everyone who is a professional ruby developer really should subscribe to his podcasts. IMO the guy is a master of OO, and his screencasts are far more valuable then 10$ and 15mins of your life per month.

Comments