Skip to content

Latest commit

 

History

History
236 lines (184 loc) · 7.93 KB

README-JS.md

File metadata and controls

236 lines (184 loc) · 7.93 KB

method-combinators

tl;dr

This library gives you some handy function combinators you can use to make Method Decorators in JavaScript (click here for examples in CoffeeScript):

  var __slice = [].slice;

  this.before = function(decoration) {
    return function(base) {
      return function() {
        decoration.apply(this, arguments);
        return base.apply(this, arguments);
      };
    };
  };

  this.after = function(decoration) {
    return function(base) {
      return function() {
        var __value__;
        decoration.call(this, __value__ = base.apply(this, arguments));
        return __value__;
      };
    };
  };

  this.around = function(decoration) {
    return function(base) {
      return function() {
        var argv, callback, __value__,
          _this = this;
        argv = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
        __value__ = void 0;
        callback = function() {
          return __value__ = base.apply(_this, argv);
        };
        decoration.apply(this, [callback].concat(argv));
        return __value__;
      };
    };
  };

  this.provided = function(condition) {
    return function(base) {
      return function() {
        if (condition.apply(this, arguments)) {
          return base.apply(this, arguments);
        }
      };
    };
  };

  this.excepting = function(condition) {
    return function(base) {
      return function() {
        if (!condition.apply(this, arguments)) {
          return base.apply(this, arguments);
        }
      };
    };
  }

The library is called "Method Combinators" because these functions are isomorphic to the combinators from Combinatorial Logic.

Back up the truck, Chuck. What's a Method Decorator?

A method decorator is a function that takes a function as its argument and returns a new function that is to be used as a method body. For example, this is a method decorator:

mustBeLoggedIn = function (methodBody) {
  return function () {
    if (currentUser.isValid()) {
      return methodBody.apply(this, arguments)
    }
  }
}

You use it like this:

function SomeControllerLikeThing() {}

SomeControllerLikeThing.prototype.showUserPreferences =
  mustBeLoggedIn(
    function() {
      //
      // ... show user preferences
      //
    }
  );

And now, whenever showUserPreferences is called, nothing happens unless currentUser?.isValid() is truthy. And you can reuse mustBeLoggedIn wherever you like. Since method decorators are based on function combinators, they compose very nicely, you can write:

triggersMenuRedraw = function(methodBody) {
  return function () {
    var __rval__ = methodBody.apply(this, arguments);
    this.trigger('menu:redraww');
    return __rval__;
  }
};

function AnotherControllerLikeThing() {};

AnotherControllerLikeThing.prototype.updateUserPreferences =
  mustBeLoggedIn(
    triggersMenuRedraw(
      function() {
        //
        // ... save updated user preferences
        //
      }));

Fine. Method Decorators look cool. So what's a Method Combinator?

Method combinators are convenient function combinators for making method decorators. When writing decorators, the same few patterns tend to crop up regularly:

  1. You want to do something before the method's base logic is executed.
  2. You want to do something after the method's base logic is executed.
  3. You want to wrap some logic around the method's base logic.
  4. You only want to execute the method's base logic provided some condition is truthy.

Method combinators make these common kinds of method decorators extremely easy to write. Instead of:

mustBeLoggedIn = function (methodBody) {
  return function () {
    if (currentUser?.isValid()) {
      return methodBody.apply(this, arguments)
    }
  }
}

triggersMenuRedraw = function(methodBody) {
  return function () {
    var __rval__ = methodBody.apply(this, arguments);
    this.trigger('menu:redraww');
    return __rval__;
  }
};

We write:

mustBeLoggedIn =
  provided(
    function() { return currentUser.isValid(); }
  );

triggersMenuRedraw =
  after(
    function() { return this.trigger('menu:redraww'); }
  );

And they work exactly as we expect:

function AnotherControllerLikeThing() {};

AnotherControllerLikeThing.prototype.updateUserPreferences =
  mustBeLoggedIn(
    triggersMenuRedraw(
      function() {
        //
        // ... save updated user preferences
        //
      }));

The combinators do the rest!

Can I use this with Node's callback-oriented programming?

This library also provides method combinators that work in an asynchronous world.

So these are like RubyOnRails controller filters?

There are some differences. These are much simpler, which is in keeping with JavaScript's elegant style. For example, in Rails all of the filters can abort the filter chain by returning something falsy. The before and after decorators don't act as filters. Use provided if that's what you want.

More specifically:

  • None of the decorators you build with the method combinators change the arguments passed to the method. The before and around callbacks can execute code before the method body is executed, but only for side-effects.
  • The provided decorator will return void 0 if it evaluates to falsy or return whatever the method body returns. There's no other way to change the return value with provided
  • The around decorator will return void 0 if you don't call the passed callback. Otherwise, it returns whatever the method body would return. You can't change its arguments or the return value. You don't need to pass arguments to the callback. If you do, they will be ignored.

Is it any good?

Yes.

Can I install it with npm?

Yes: npm install method-combinators

Anything you left out?

Yes, there are some extra combinators that are useful for things like error handling and design-by-contract. Read the source for yourself. Or have a gander at these blog posts:

I've written a book called JavaScript Allongé that discusses combinators and decorators in depth. Check it out!

Et cetera

Method Combinators was created by Reg "raganwald" Braithwaite. It is available under the terms of the MIT License. The retry and condition combinators were inspired by Michael Fairley's Ruby method_decorators.