Archive for March, 2013

March 20, 2013

For-comprehensions in JavaScript

I’m starting to learn Scala, and I love their for-comprehension style of monadic programming. Here’s a small library that uses eval and textual manipulations to do desugaring.

// A Monad class for Array and other monads to inherit from

function Monad() {
  throw "abstract";
}

Monad.prototype.flatMap = function (f) {
  return this.map(f).flatten();
};

// Make Array inherit from Monad
Array.prototype.__proto__ = Monad.prototype;

// Define the monadic multiplication
Array.prototype.flatten = function () {
  var result = [];
  var len = this.length;
  for (var i = 0; i < len; ++i) {
    result = result.concat(this[i]);
  }
  return result;
};

// The comprehension class
var M = function () {
  var pairs = [];
  this.for = function (v, e) {
    pairs.push({e: e, v: v});
    return this;
  };
  this.yield = function (body) {
    var desugared = pairs.map(function (pair, index) {
          return '(' + pair.e + ').' +
              ((index === pairs.length-1) ? 'm' : 'flatM') +
              'ap(function(' + pair.v + ') { return ';
        }).join('') + '(' + body + ');'
        + pairs.map(function () { return ' });' }).join('');
    pairs = [];
    return desugared;
  };
};

// Avoid the need to say (new M) at the start
M.for = function (v, e) {
  return (new M).for(v, e);
};

// Tensor product of lists
// eval(M.for('x','[1,2,3]').for('y','[5,6,7]').yield('x * y'));
// 5,6,7,10,12,14,15,18,21

// Aka Maybe
function Option() {
  throw "abstract";
}
Option.prototype = Object.create(Monad.prototype);
Option.prototype.map = function (f) {
  if (this instanceof None) { return this; }
  return new Some(f(this.value));
};
Option.prototype.flatten = function () {
  if (this instanceof None) { return this; }
  return this.value;
};
function None() {}
None.prototype = Object.create(Option.prototype);
None.prototype.toString = function () { return 'None'; };
function Some(v) {
  this.value = v;
}
Some.prototype = Object.create(Option.prototype);
Some.prototype.toString = function () {
  return 'Some(' + this.value + ')';
};

// var xs = [1,2,3];
// eval(M.for('x','xs').for('y','x % 2 ? new Some(x) : new None()').yield('x * y'));
// [Some(1), None, Some(9)]

The call to eval on the outside is necessary to capture the scope in which the for-comprehension occurs (e.g. the scope in which xs occurs in the second example).

Advertisements