Ruby – #tap that!

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!

Tags: ,

  • 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.

  • Mark Coates

    Hmm. Your examples could easily be re-written as:

    object = Model.new.save!

    object = SomeClass.new{ |obj| obj.key = ‘value’}

    # and
    [1,2,3].reverse!

    All of the examples return the original object, so tap is unnecessary and superfluous—or am I missing the point here? What are some other examples?

    • http://seejohncode.com/ John Crepezzi

      sure, here’s one:

      [1, 2, 3].tap(&:compact!).reverse # would error if non-tapped since compact took no action

    • Mark Coates

      Gotcha. I also saw a follow-up article you wrote regarding this. I dig it. :)

      Cheers!