Skip to content

Instantly share code, notes, and snippets.

@genericallyloud
Created October 21, 2013 16:03
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save genericallyloud/7086380 to your computer and use it in GitHub Desktop.
Save genericallyloud/7086380 to your computer and use it in GitHub Desktop.
A potential solution for doing scoped binding of methods to types using a technique similar to Clojure protocols instead of Ruby refinements.

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"
    
    
}
@spion
Copy link

spion commented Nov 19, 2013

I love it. This will open a whole new world of possibilities for library designers.

@spion
Copy link

spion commented Nov 20, 2013

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?

@domenic
Copy link

domenic commented Nov 27, 2013

FYI there is no module 'x' { } syntax; you must use separate files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment