23

Using Underscore.js, I can write the following which returns 42:

_([42, 43]).chain()
    .first()
    .value()

I have custom function, not part of Underscore.js called double():

function double(value) { return value * 2; };

I would like to be able to call this function in an Underscore chain, as if it was part of Underscore. I would like to write the following, which I'd like to return 84:

_([42, 43]).chain()
    .first()
    .double()
    .value()

This can't work since Underscore doesn't define double(). I could use tap() as in:

_([42, 43]).chain()
    .first()
    .tap(double)
    .value()

This is valid, but tap applies the function to its argument and returns the argument, not the result of the function. So it looks to me like I would need a sort of tap that returns the result of the function applied to its argument. Is there anything like this in Underscore.js? Am I missing something terribly obvious?

4
  • 4
    Be aware that double is a future reserved word and implementations may throw a SyntaxError if it's used as an Identifier. Commented Oct 15, 2010 at 20:51
  • Alessandro, have you figured out yet that I gave you the only correct answer to this question? What I laid out does not pollute the underscore namespace at all. The underscore.js library anticipated what you asked for and built in a feature for exactly that. Commented Dec 16, 2010 at 7:22
  • Underscore now has both tap (runs a function to modify each item) and map (runs a function that returns a new item). Commented Aug 21, 2014 at 15:05
  • @CMircea As of 2014-08-25, I don't see any indication that tap() can be used to modify the wrapped object. Am I missing something? underscorejs.org/#tap Commented Aug 25, 2014 at 23:49

7 Answers 7

25

Not finding a tap that returns the value returns by the function is runs, I define one which I can take and add to _:

_.mixin({take: function(obj, interceptor) {
    return interceptor(obj);
}});

Then assuming I have:

function double(value) { return value * 2; };

I can write:

_([42, 43]).chain()
    .first()             // 42
    .take(double)        // Applies double to 42
    .value()             // 84

You can look at take as map on objects, instead of lists. Want to experiment with this? See this example on jsFiddle.

Sign up to request clarification or add additional context in comments.

4 Comments

apply might be a better name
@Gabe, I thought of calling it apply, but the name is already defined on functions, and it has a different semantic: f.apply(obj, argArray) instead of obj.take(f, argArray). You don't think this could be confusion? (Honestly asking as I am not convinced myself either way.)
For those looking, tap is now part of underscore core.
A little late to the party, but how about naming it tapAndReturn?
5

So you have a custom function:

function double(value) { return value * 2; }

You can use mixin to extend Underscore with it:

_.mixin({ double:double });

Now you can call your function from the Underscore object _:

_.double(42); // 84

and from the wrapped object returned from chain:

_([42, 43]).chain()
  .first()
  .double() // double made it onto the wrapped object too
  .value(); // 84

2 Comments

yes, that's one way of doing it. Something I don't like about this is that it pollutes _. I.e. this isn't workable if you use chaining all over the place in you code, and hence have a lot of those functions.
I suggested an alternative that requires me to add just one function to _ - see the answer to my own question. I will experiment with this and see how well this works in practice.
5

Alright, I'm fresh off of reading the underscore annotated source code for the first time. But I think you can do something like this:

function double(value) { return value * 2; };

var obj = _([42, 43]).addToWrapper({double:double});

obj.chain()
  .first()
  .double()
  .value();

The syntax/details might not be right, but the core point is this: when you call _([42,43]), you're calling underscore as a function. When you do so, it instantiates a new object and then mixes into that object most of the underscore functions. Then, it returns that object to you. You can then add your own functions to that object, and none of this pollutes the "_" namespace itself.

That's what the underscore.js code looked like to me. If I'm wrong, I'd like to find out and hopefully someone will explain why.

EDIT: I've actually been using underscore.js heavily for about a month now, and I have gotten pretty familiar with it. I now know it behaves like I said here. When you call _ as a Constructor function, you get back your own "namespace" (just an object), and you can add things to it with addToWrapper() that show up in your namespace but not in the "global" "_" namespace. So the feature the OP wanted is already built in. (And I have been really impressed with underscore, btw, it is very very nicely done).

3 Comments

Charlie, yes, you could do this. I should have mentioned this in my question: I don't want to do it as it would pollute the _ namespace. (Imagine if you have a large codebase using Underscore, and each time you want to do this you need to add the function to Underscore.)
@Alessandro Vernet - no it will not pollute the _ namespace. That is the beauty of it. When you call _([42,43]), it returns a brand new namespace just for you. When you then add "double" to it, your own namespace will have double, but _ itself will not.
@Cesar Canassa - fair enough. That's why I said you can do something "like" this. I wasn't in front of my development machine with all the tools installed when I typed this. The point is that calling _ as a function returns a new object and won't pollute the "usual" _ namespace. Turns out, addToWrapper does not return this new object, so you have to add another line, as in: var obj = _([42, 43]); obj.addToWrapper(whatever); obj.chain().first().double().second(). And even that I can't certify as exactly right. The concepts are right, but I don't have a convenient place to check exact syntax.
1

Looks like lodash has implemented exactly what you are looking for:

_.thru(value, interceptor)

from the docs:

This method is like _.tap except that it returns the result of interceptor

https://lodash.com/docs#thru

Comments

0

Many ways to easily achieve this, here is one such solution:

  _.chain([42,43])
    .first(1)
    .map(double)
    .first()
    .value();
    // 84

However, I would recommend using Ramda then with auto-curry and pipe / compose these kinds of tasks are trivial:

R.pipe(R.head, R.multiply(2))([42, 43]);  // 84

equivalent to:

R.compose(R.multiply(2), R.head)([42, 43]);  // 84

If you wanted to extend the solution to take say the first 2 items, instead of a single value, as requested by the OP, then:

R.pipe(R.take(2), R.map(R.multiply(2)))([42, 43])  // [84, 86]

However, in Ramda R.compose is preferred. Either way, for trivial tasks like this, it does tend to be more convenient and easy to use.

Comments

-1

Does map work for this?

_([42, 43]).chain()
    .first()
    .map(double)
    .value()

edit

from the documentation, it looks like map would only work if you place it before the call to first:

_([42, 43]).chain()
    .map(double)
    .first()
    .value()

2 Comments

right, map doesn't cut it as it only works on list. Here I need a sort-of map that works on single "items".
@avernet you could simply prefix and postfix map with first.
-1

Using compose is another way dealing with the situation, but I think adding a function such as take as I suggested earlier is a better solution. Still, here is how the code would look like with compose:

function double(value) { return value * 2; };

_.compose(
    double,
    _.first,
    _.bind(_.identity, _, [42, 43])
)();

The initial value needs to be provided through a function which returns that value (here done by currying identity), and the functions need to be listed in an other which is the reverse of what you have with a chain, which appears as pretty unnatural to me.

1 Comment

I agree that this is way more ugly than the other solution you posted.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.