The following example uses the '::' operator which has a proposal already and a champion on the committee. Other than the bind operator, the example I give uses no new syntax (other than what is new to ES6), and could fairly easily be polyfilled - though I expect it could be optimized if implemented natively. If you haven't seen Clojure protocols, they allow for single dispatch polymorpism based on type, without the methods being defined as part of that type. In clojure, they use the first parameter, but for my proposal, it uses the receiver (this), and uses the bind operator to make the call look similar to a normal method call. While I haven't actually written an implementation of the Protocol class I demonstrate here, I've thought enough about it that I feel it could be done in a few different ways without too much complexity.
/**
* The following module is a sampling of underscore methods converted to use the unbound method style
* for use with the :: operator, and also show off the Protocol concept I'm adapting from Clojure to
* fit with JavaScript
*/
module 'UnderscOOre' {
// so this is just an idea of what we can do without additional syntactic
// support for protocols. The arguments to Protocol are the names of the
// methods belonging to the protocol. Under the hood, a function will be generated
// for each name passed in, which can do the dispatch logic as follows:
// 1. if the receiver has an own property of the same name, use that
// 2. if the receiver's type matches an extension of the protocol, use the protocol method
// 3. if the receiver has a method of the same name somewhere up the prototype chain, use that
// 4. use the default if available
// Also note that I export the Collections protocol so that it can potentially be extended to other data types
export const Collections = Protocol("map","each","shuffle","pluck");
// destructure the functions added to the protocol to make them available locally and
// also export them (I think this is the right export syntax)
export const {map,each,shuffle,pluck} = Collections.methods;
// these are just a couple of things in the underscore code - ignore
let breaker = {};
export function random(min, max) {
if (max == null) {
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1));
};
// the defaults of a Protocol are protocol methods which might already be polymorphic without needing dispatch
// or are able to be defined in terms of other protocol methods. Notice this is logically
// very similar to traits or mixins where a sometimes all it takes is a couple of key methods to
// leverage several others
Collections.defaults({
each(iterator, context){
if (this.length === +this.length) {
for (var i = 0, length = this.length; i < length; i++) {
//notice we also get to use :: for a simple call replacement
if (context::iterator(this[i], i, this) === breaker) return;
}
} else {
var keys = this.keys();
for (var i = 0, length = keys.length; i < length; i++) {
if (context::iterator(this[keys[i]], this[i], this) === breaker) return;
}
}
},
pluck(key) {
// can expect receiver to implement full protocol including map
return this::map(value => value[key]);
},
shuffle(obj) {
var rand;
var index = 0;
var shuffled = [];
// again, using other parts of the protocol lets this be generic
this::each(function(value) {
rand = random(index++);
shuffled[index - 1] = shuffled[rand];
shuffled[rand] = value;
});
return shuffled;
};
});
// To extend a protocol for dispatching against a type, use the extends method of the protocol
// to supply implementations of the methods
Collections.extend(Object, {
// for all these methods, |this| will refer to the object instance
map(iterator, context) { //note that using the binding operator would make this signature pointless
var results = [];
if (this == null) return results;
// to call other protocol methods, keep using the :: operator and the protocol functions
this::each((value, index, list) => {
results.push(context::iterator(value, index, list));
});
return results;
}
});
// can extend a protocol to other types as well
Collections.extend(Array, {
//notice that we can skip map because its implemented already
//each can just be an alias for native forEach, and override the default
each:Array.prototype.forEach
});
// Here's the protocol for just arrays
export const ArrayLike = Protocol("first","initial","last","rest");
export const {map,each,shuffle,pluck} = ArrayLike.methods;
// these would probably be mostly done in defaults and use standard arraylike techniques
ArrayLike.defaults({
//we can just pretend I actually finished writing these, right?
first(){},
initial(){},
last(){},
rest(){}
});
}
/**
* This module demonstrates importing and using the underscore
*/
module 'UsingUnderscOOre' {
// I can see how this could get a little tedious, but its not so bad, especially
// if you only import what you actually use
import {map,every,shuffle,pluck,first,initial,last,rest} from 'UnderscOOre';
// this is just an object, and should work with the protocol
let someObj = {a:1,b:2,c:3};
someObj::map( n => n * 3;) //return [3,6,9]
// an array, also has the protocol defined
let someArr = [1,2,3];
someArr::map( n => n * 3;) // return [3,6,9]
// this is just a dumb class to demonstrate how
// protocols can interact with existing methods
class DoingMyOwnThing {
constructor(name){
this.name = name;
}
map(){
return this.name;
}
}
let dmot = new DoingMyOwnThing("Bob");
dmot::map( n => n * 3;) // returns "Bob"
}
By the way, in the absence of the bind operator, I wrote this little CommonJS module - https://github.com/spion/modular-chainer
The syntax is not nearly as neat (although performance is decent for most use cases). Do you think that protocols can be implemented on top of it?