Tap versus intermediate variables
June 22, 2012 📬 Get My Weekly Newsletter ☞
In my Ruby style guide, I mention the preference for using Ruby’s tap
:
When you must mutate an object before returning it, avoid creating intermediate objects and use tap:
I thought it might be interesting to expand on this.
What is tap
?
First off, tap
is a method on Object
that takes a block, which is passed itself, and evaluates to itself. Whoa.
class Object
# Imagined implementation
def tap(&block)
block.call(self)
self
end
end
An interesting use of tap
is when debugging a Law of Demeter violation:
# before
@person.parents.first.nag(:are_we_there_yet?)
# after
@person.parents.tap { |parents| puts parents.inspect }.first.nag(:are_we_there_yet?)
No matter what happens inside of the block you give to tap
, the call always evaluates to the object itself. We don’t change the string of calls, but can inject code into it.
This is not the power of tap
in my opinion. I use tap
when:
- I’m writing a method that creates and returns an object
- I must modify or call methods on that object before returning it
Intermediate Variable Elimination Front
From my last blog post, I had this method:
def create_new_user(params)
User.create(params).tap { |new_user|
if new_user.valid?
UserMailer.deliver_welcome_email(new_user)
end
}
end
Between creating the user and returning it, I needed to do some other stuff, so I create a scope in which to do it with tap
.
The classic approach is to use an intermediate variable for the new user and looks like so:
def create_new_user(params)
new_user = User.create(params)
if new_user.valid?
UserMailer.deliver_welcome_email(new_user)
end
new_user
end
Same lines of code, so why is the tap
version better?
From my style guide1:
Intermediate objects increase the mental requirements for understanding a routine.
tap
also creates a nice scope in which the object is being mutated; you will not forget to return the object when you change the code later
Let’s look at our intermediate routine again, this time, marking a few places in the code
def create_new_user(params)
new_user = User.create(params)
if new_user.valid?
UserMailer.deliver_welcome_email(new_user)
end
# 1
new_user
# 2
end
- Here is where any new code should be added to this method.
- Here is where you might add new code that will cause this method to break
A developer’s instinct is to add new code “at the bottom”. In the “intermediate variables” version, the last line is special, so you have to add code on the second to last line.
But, isn’t that the same in the tap
version? No, because tap
creates a scope, visually and literally.
def create_new_user(params)
User.create(params).tap { |new_user|
if new_user.valid?
UserMailer.deliver_welcome_email(new_user)
end
# 1
}
end
The location of #1 is logically “the bottom”, because it’s the end of the scope in question, and thus where you are more
likely to put new code. It also alleviates you from having to worry about making sure that new_user
is the last thing
evaluated; tap
handles that. No matter what new code you add, the new user is always returned.
I find this simple little thing makes certain routines easier to understand and modify.
Appendix: What if I don’t write Ruby code?
In Scala, this can be achieved using implicits:
object Tapper {
implicit def anyToTapper[A](obj: A) = new Tapper(obj)
}
class Tapper[A](obj: A) {
def tap(code: A => Unit): A = {
code(obj)
obj
}
}
import Tapper._
new User(params).tap { newUser =>
if (newUser.isValid) {
UserMailer.deliverWelcomeEmail(newUser)
}
}
In JavaScript, you can just put it on Object
:
Object.prototype.tap = function(block) { block(this); return this; };
function createNewUser(params) {
return new User(params).tap( function(newUser) {
if (newUser.isValid()) {
new UserMailer().deliverWelcomeEmail(newUser);
}
});
}
In Java, you’re screwed, but just for fun, let’s try:
public interface TapFunction<T> {
void apply(T object);
}
public class Tapper {
public static <T> T tap(T object, TapFunction<T> function) {
function.apply(object);
return object;
}
}
import static Tapper.tap;
public void createNewUser(Map<String,?> params) {
tap(new User(params),new TapFunction<User>() {
public void apply(User newUser) {
if (newUser.isValid()) {
UserMailer.deliverWelcomeEmail(newUser);
}
}
});
}
Not exactly a huge win in Java :) Now, who’ll write this in C?