Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Allow Iterators to be Methods (ie. Merge Invoke With Other Array/Collection Methods) #631

Closed
machineghost opened this Issue · 8 comments

2 participants

@machineghost

Currently all of the wonderful Underscore array/collection methods take an iterator function. This is great when you want a single iterator function, eg:

_.filter([1,2,3,4], function(num) {
    return num % 2 === 0;
});

However, it's not so great when you want to invoke a method on the objects you're iterating through, so that you get different functions for different types of objects:
_.filter(objArray, obj.isValid); // This could filter invalid objects out ... if it worked

What would be really awesome is if there was a way to use such iterators; imagine:
_.filter(objArray, 'isValid'); // This could filter invalid objects out ... if implemented

Now, obviously that's impossible when using the native browser functions, but since Underscore has fallback implementations of all of those functions, you could just rely on the fallbacks if the user supplies a string iterator.

In terms of implementation, you would basically just need to do something like:

var each = _.each = _.forEach = function(obj, iterator, context) {
    if (obj == null) return;
    if (!typeof(iterator) == 'string' && nativeForEach && obj.forEach === nativeForEach) {
        obj.forEach(iterator, context);
    } else if (obj.length === +obj.length) {
        for (var i = 0, l = obj.length; i < l; i++) {
            var currentIterator = typeof(iterator) == 'string' ? obj[iterator] : iterator;
            if (i in obj && currentIterator.call(context, obj[i], i, obj) === breaker) return;
        }
...

If this was implemented, no longer would you have to have an iterator that distinguishes between Foos and Bars, and adjusts the logic accordingly; instead you could simply put one iterator method on class Foo, another on class Bar, and Underscore could be completely oblivious as to their different implementations.

@machineghost

Whoops:
!typeof(iterator) == 'string'
should have been:
typeof(iterator) != 'string'

Hopefully the general idea was conveyed anyway.

@jashkenas
Owner

How about:

_.filter(objArray, function(obj){ return obj.isValid(); });

"Oh, but function(){ return; } is too much boilerplate to type", you may object. And you'd be quite right. In CoffeeScript:

_.filter objArray, (obj) -> obj.isValid()
@jashkenas jashkenas closed this
@machineghost

Wouldn't that have to be:
_.filter objArray, obj -> obj.isValid()
?

Regardless, is Underscore a CoffeeScript library or a Javascript library? If it is indeed a Javascript library, wouldn't it be useful to Javascript users to have this syntax? Or even if it is a CoffeScript library, isn't:

_.filter objArray, 'isValid'
better than:
_.filter objArray, obj -> obj.isValid()
?

There doesn't appear to be any downside whatsoever to this proposal, and it would make Underscore better for at least a subset of its users ... why are you so resistant to improving Underscore?

@machineghost

Just to clarify what I mean by "resistant", in most open-source projects new feature suggestion responses fall in to one of three categories:

  • 1. No: the proposed feature wouldn't work, wouldn't fit the library, etc.
  • 2. No: I don't feel like writing this feature for you; submit a patch with your request and we'll talk
  • 3. Yes: This is such a great idea I'm going to implement it

However, I have submitted several feature requests for Backbone/Underscore, and read the responses to many others, and they all (except for a couple) have gotten a response of:

  • 4. This feature fits the library, is implementable, would be useful, etc. ... but I refuse to even entertain a patch for it.

Now if Underscore is just your personal library, that you're generous enough to share with the rest of us, this is a perfectly reasonable response. If Underscore is actually an open-source project though, this kind of response is mystifying; don't you want the library to be more useful to more people?

@jashkenas
Owner

Ah, sorry to have been brief -- here's the full explanation:

The category under which this idea falls is 1 ... and I believe it's been discussed before.

Basically we're talking about taking this pattern:

_.function(list, function(item){ return item.method(); });

... and shortening it to this:

_.function(list, 'method');

... which is all well and good, but what if you wanted to return the property instead of the method:

_.function(list, function(item){ return item.property; });

... and what if you wanted to pass an argument to the method:

_.function(list, function(item){ return item.method(arg); });

... and what if you wanted to pass more than one argument, or pass the method into a function, or do something fancier:

_.function(list, function(item){ return method(item); });

... none of these use cases are supported by prioritizing passing a string as "call the named method with zero arguments" -- and some of them are arguably more common and useful. We don't want to add a special case check to each Underscore function to support something quite so limited. Hope that makes sense.

@machineghost

First off, thank you much for the explanation; it was greatly appreciated. And if you're tired of discussing this issue, by all means please feel free to tell me "go search the bug reports instead of bothering me you lazy bastard" ;-)

However, if you're willing to consider a counter-argument, almost all of the cases you mentioned can be supported:

1) Properties instead of methods

... which is all well and good, but what if you wanted to return the property instead of the method:

Solution: Keep the same exact syntax from the user's perspective, but behind the scenes, instead of:

// Starting at line 96(ish), in the code for _.map
each(obj, function(value, index, list) {
    results[results.length] = iterator.call(context, value, index, list);
});

use:

each(obj, function(value, index, list) {
    var currentIterator = typeof(iterator) == 'string' ? obj[iterator] : iterator;
    if (typeof(iterator) != 'function') {
        results[results.length] = currentIterator; // iterator is just a property value
    } else {
        results[results.length] = iterator.call(context, value, index, list);
    }
});

2) Passing extra arguments to the method

... and what if you wanted to pass an argument to the method:

Solution: This can be handled the same way _.bind handles it. Currently all of the functions we're talking about have a signature pattern that's essentially "someArgs, context" (either "list, iterator, context" or "list, iterator, extraArg, context").

The bind function has a similar pattern, but with extra passed-along arguments: "someArg, context, passedAlongArgs".

The Underscore collection functions could thus follow bind's pattern:

_.map(someList, 'nameOfMethod', context, passedAlongArg1, passedAlongArg2, etc.)

_.filter(pets, 'isSpecies', null, 'feline'); // filters cats via pet.isSpecies('feline')

Heck, this could even be useful with the existing function args:

var isSpecies = function(pet, desiredSpecies) {
    return pet.species == desiredSpecies;
}
_.filter(pets, isSpecies, null, 'feline'); // filters cats via isSpecies(pet, 'feline')

3) Solving specialized/outside-this-proposal cases

... and what if you wanted to pass more than one argument, or pass the method into a function, or do something fancier:

Solution: This proposal is all about making it easier to handle cases where the function is a method on the objects being iterated through. Saying that it doesn't solve cases where the method isn't on the object isn't really a fair argument (the standard for new improvements to Underscore can't be that they must solve every possible problem, or you're not going to get many improvements).

However, the case you proposed could totally be handled by this new syntax, as long as you move the function on to the object. In other words "method(item)" is essentially the same as "item.method()"; in both cases method has access to item (it's just "arguments[0]" vs. "this"). Therefore, if you rewrite your example you could do:

// Old Code (with extra lines added for clarity):
var Item = function(foo) {
    this.foo = foo; 
};
var method = function(item) {
    return item.foo + 1;
}
_.function(list, function(item){ return method(item); });

// New Code:
var Item = function(){};
Item.prototype.method = function(){
    return this.foo + 1;
}
_.function(list, 'method');

One last thought: none of this proposal is really new. Backbone events has already "pioneered" the idea of taking an argument that's either a function or method name (eg. {"click" : "clickHandler"}). Bind has already pioneered the idea of taking extra arguments (after the context argument) and passing them through to an invoked function.

All I'm saying is "hey, these ideas worked great in other places, but now that I'm used to having their convenience elsewhere, is there any chance of getting the same convenience in the Underscore collection methods?"

@jashkenas
Owner

Yep -- it's a very neat idea. It's just not a direction that Underscore collection functions should head ... the core idea with the API is to provide useful generic functions that take "iterator" functions as arguments -- the functions that specify exactly what the comparison or value should be. We don't want to stray into the domain of "string programming", which, while certainly more terse, ends up being limited as to the types of expressions you can express.

For a library that does similar things to Underscore, but embraces string programming, check out Functional.js: http://osteele.com/sources/javascript/functional/

You have nice terse things like this:

reduce('x*2+y', 0, [1,0,1,0])
@machineghost

Heh, thanks for the suggestion, but I try to avoid using a million libraries, and since Backbone and Underscore are integrated (and we use Backbone) I'm kinda stuck with it. Maybe I can make some sort of plug-in ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.