Skip to content

Instantly share code, notes, and snippets.

@ordinaryzelig
Last active April 15, 2017 17:48
Show Gist options
  • Save ordinaryzelig/12621773dc0db5fa1f85 to your computer and use it in GitHub Desktop.
Save ordinaryzelig/12621773dc0db5fa1f85 to your computer and use it in GitHub Desktop.

(steps on soap box)

I think Ruby's Object#tap method is abused. I see code like this all the time:

obj = SomeClass.new.tap do |o|
  o.blah = true
end

What good does tap do here? It saves 0 lines of code. It makes it complex for no reason. And if it's a particularly long block, it just makes it harder to see what actually gets returned. To me, it feels like trying to be clever for the sake of being clever.

Compare it to the "traditional" way:

obj = SomeClass.new
obj.blah = true
obj

Same number of lines of code. Same return value. Only 1 local variable (as opposed to needing to come up with 2 different names like above with obj and o). No unnecessary complexity. I prefer this simpler version.

(steps off soap box)

Here's a better, more appropriate use of tap:

list
  .map { |o| o.foo.bar }
  .reverse
  .more_transform           .tap { |things| puts "transformed: #{things.inspect}" }
  .even_more_transformations

...as if to "tap into" a method chain (to quote the official Ruby documentation).

This can be useful with debugging a series of ActiveRecord chained scopes, e.g.

User
  .active                      .tap { |users| puts "Users so far: #{users.size}" }
  .non_admin                   .tap { |users| puts "Users so far: #{users.size}" }
  .at_least_years_old(25)      .tap { |users| puts "Users so far: #{users.size}" }
  .residing_in('USA')

This makes it super easy to debug at any point in the chain without having to store anything in in a local variable nor requiring much altering of the original code.

And lastly, use it as a quick and unobtrusive way to debug without disrupting normal code execution:

def rockwell_retro_encabulate
  provide_inverse_reactive_current
  synchronize_cardinal_graham_meters
  @result.tap(&method(:puts))
  # Will debug `@result` just before returning it.
end
@bparanj
Copy link

bparanj commented Jan 6, 2016

How is this better that just doing 'puts result' ?

@jhoffner
Copy link

why not just

# one line of code
SomeClass.new.tap { |o| o.blah = true }

# or use cases like this
obj ||= SomeClass.new.tab do |o|
  o.blah = true
  o.whatever = true
end

@bensheldon
Copy link

Trivial examples are trivial. Usually I find myself using tap in order to encapsulate an object's simple properties inside of a complicated context, or to encapsulate logic. Like so:

@thing_i_hate = Thing.new.tap do |thing|
  thing.name = 'Apple'
  thing.type = 'fruit'
end

@thing_i_love = Thing.new.tap do |thing|
  thing.name = 'Pear'
  thing.type = 'fruit'
  if today == :tuesday
    thing.share = true
  else
    thing.share = false
  end
end

...which makes it easy to copy-paste properties between the objects because they are consistent within the tap blocks. I find the wrapping logic to make it easier for me to understand what is happening because the context is very constrained. I find this especially useful when writing RubyMotion and defining UIViewControllers or whatever where I just tap them as view but their they're in the exterior context as some really long variable name.

But yes, I've also seen tap abuse. Though it's often because someone is pre-expecting additional complexity, or because they removed complexity but didn't do a full refactor.

Thanks for sharing!

@elia
Copy link

elia commented Jan 15, 2016

Nice post!

Beyond the point of the article, but I think it's worth pointing out that p returns the value passed, so:

return thing

and

return p(thing)

and

return thing.tap(&method(:puts))

all return the same value.

@davidimoore
Copy link

I think one example of using tap to initialize in a practical way is the following:

some_confirming_variable = true

OpenStruct.new(confirmation: some_confirming_variable).tap do |os|
if(some_confirming_variable)
    os.sooner = "sooner"
    os.later = "later"
    os.you_get_it = "yeah"
  end
end

@austinthecoder
Copy link

@ordinaryzelig Nice article. I often use tap for Array#uniq! because the return value isn't consistent. It returns nil when nothing was changed. For example, list.tap &:uniq! will always return the list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment