Ruby – #tap that!

January 2, 2012

Today I wanted to talk about #tap, an addition to Ruby in versions >= 1.9, and how I’ve been using and seeing it used lately.

What is #tap?

The feature is coded like so:

VALUE rb_obj_tap(VALUE obj)
{
  rb_yield(obj);
  return obj;
}

So, in Ruby:

class Object
  def tap
    yield self
    self
  end
end

What is #tap for?

So that looks pretty simple, it just allows you do do something with an object inside of a block, and always have that block return the object itself. #tap was created for “tapping” into method chains, so code like this:

def something
  result = operation
  do_something_with result
  result # return
end

can be turned into (without modifying the contract of do_something_with):

def something
  operation.tap do |op|
    do_something_with op
  end
end

Where has it gone wrong?

Some early blog posts talking about tap focused on its use for inserting calls to putsinto code without modifying behavior:

arr.reverse # BEFORE

arr.tap { |a| puts a }.reverse # AFTER

This isn’t a great use, not only because it involves debugging your code solely by means of output (instead of a debugger), but also because #tap is so much cooler an idea than just a mechanism for inserting temporary code.

Why I like it:

In additional to the tapping behavior described in the first section, here are some other uses I’m seeing / using:

Assigning a property to an object

Especially useful when assigning a single attribute

# TRADITIONAL
object = SomeClass.new
object.key = 'value'
object

# TAPPED
object = SomeClass.new.tap do |obj|
  obj.key = 'value'
end

# CONDENSED
obj = SomeClass.new.tap { |obj| obj.key = 'value' }

Ignoring method return

Useful when wanting to call a single method on an object and keep working with the object afterwards:

# TRADITIONAL
object = Model.new
object.save!
object

# TAPPED
object = Model.new.tap do |model|
  model.save!
end

# CONDENSED
object = Model.new.tap(&:save!)

Using in-place operations chained 

A lot of times, we expand logic in order to use in-place methods like reverse!, but look:

# TRADITIONAL
arr = [1, 2, 3]
arr.reverse!
arr

# TAPPED & CONDENSED
[1, 2, 3].tap(&:reverse!)

Conclusion:

Just like anything else, don’t overuse #tap just for kicks. Its tempting to tap everything, but it definitely can make code less readable if used inappropriately. That being said, its a great addition to a Rubyist’s sugar toolkit. Give it a shot!

  • Pingback: Using Ruby's #tap and #to_proc Together for in-place Operations

  • grzlus

    So to first example

    User.new {:key => ‘value’ }

    Second:

    User.create!

    And third:

    [1,2,3].reverse!

    • http://seejohncode.com/ John Crepezzi

      Thanks for the comment

      The first two are fine if you always can control the API you’re programming against.

      The third is not always the same, especially on methods like compact! that have different behavior based on if something actually changed.