The Underscore.js documentation explains that the _.tap()
function "taps" into a method chain. http://underscorejs.org/#tap
I have trouble following their example:
_.chain([1,2,3,200])
.filter(function(num) { return num % 2 == 0; })
.tap(alert)
.map(function(num) { return num * num })
.value();
=> // [2, 200] (alerted)
=> [4, 40000]
What is the method chain in this context? I always thought of method chaining as the concept of chaining methods off of one another: object.foo().bar().baz()
.
I have seen examples using this method: module.exports = _.tap {}, (connectors) ->
, so does this "tap" into the object literal's method chain?
From the fine manual:
Chaining
[...]
Calling chain will cause all future method calls to return wrapped objects. When you've finished the computation, use value to retrieve the final value.
So chaining in the Underscore context is the same as chaining elsewhere except that you're chaining Underscore methods on a wrapped object. You start by calling _.chain
to get your wrapper:
_(obj).chain()
then you call Underscore methods on what _.chain
returns and they will return wrapped objects as well:
_(obj).chain()
.filter(function(x) { ... })
.map(function(x) { ... })
...
and finally you call _.value
to unwrap the chaining-wrapper:
var result = _(obj).chain()
.filter(function(x) { ... })
.map(function(x) { ... })
...
.value();
Back to _.tap
. All tap
does is this:
_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
so it calls the passed function, interceptor
on the value being iterated over and returns what is being iterated over without doing anything to it. _.tap
is the same as:
.map(function(x, func) { func(x); return x })
but it makes your intention clear. In effect, _.tap
lets you peek into the data passing through the method chain without altering that data.
I agree the example in the docs is stupid as alert
ing the iterated object is never something you'd want to do in a real situation (unless you were debugging and wanted to know the state of the object between 2 chained methods — but in this scenario it would probably just alert [object Object]
; console.log
would be more appropriate but wouldn't work as intended [but I digress]). Here's an example of a non-chaining workflow:
var x = { a: 1, b: 2, c: 3 };
x = _.filter( x, function( value ){
return ( value % 2 === 0 );
} );
x.length = _.size( x );
x = _.pairs( x );
This scenario really lends itself to chaining: all the code above is essentially creating and serially modifying one object. Chaining makes that clear, and stops you continuously writing assignments. The only problem is the line where I declare length
— that doesn't fit neatly into the chain, since it's the only statement there that's not simply running an underscore method on the object and assigning the result back to that object. And this is where tap
comes in: you want to do something that doesn't lend itself easily to chaining within the chain. Here's how he chained workflow looks:
var x = _
.chain( { a: 1, b: 2, c: 3 } )
.filter( function( value ){
return ( value % 2 === 0 );
} )
.tap( function( x ){
x.length = _.size( x );
} )
.pairs()
.value();
The chain in your example is filter().tap().map()
:
_.chain([1,2,3,300])
wraps an array in an underscore wrapper that has methods like filter
, tap
, map
, value
, etc.
.tap(alert)
unwraps the result from filter()
, calls alert()
with the unwrapped object, and then wraps it up again and returns it
.value()
returns the wrapped object (minus the wrapper).
Method chaining, as you describe, chains methods off each other. The key detail, with regard to _.tap
is that the results of each method are passed on to the next.
_.tap lets you insert a method whose results aren't passed through. The result of the previous method is passed in to the method following _tap
. This allows for processing of intermediate results within a chain without changing the results of the chain.
In your example the alert
would break the chain if called alone, since it doesn't return anything. Using _.tap
, causes the results of the previous method (filter(function(num) { return num % 2 == 0; })
) to be passed through to the following method (map(function(num) { return num * num })
)