Something like monads from afar

After reading dozens of “the most understandable introductions to monads” and reading (also) dozens of discussions in different forums, I came to the conclusion that there is a group of abstract OO programmers, whom my interpretation of “something like monads” can help get a little closer to the correct one understanding.

So, in this publication you will not find answers to the following questions:
1. What is a monad?
2. Where and how to use monads?
3. Why are monads better than their absence?

In programming, there is such a phenomenon - “design patterns”. Officially, this is a set of best practices that should be followed when solving "typical problems". Unofficially, it's just a bunch of crutches for languages ​​that don't have built-in tools to solve common problems.

There is such a design pattern - Interpreter . First of all, it is remarkable because it allows you to make some kind of virtual machine on top of your favorite programming language, while:

1. You can describe the program in a language that the virtual machine understands.
2. It is possible to rob the corovans in all details to describe how the virtual machine should interpret each instruction.

All that is written below makes sense only if the kind reader is minimally familiar with the mentioned pattern.

A comparatively canonical example:
function add(x) {
  return { op: "add", x: x };
}
function div(x) {
  return { op: "div", x: x };
}
function run(value, statements) {
  for(var i = 0; i < statements.length; ++i) {
    var statement = statements[i];
    var op = statement.op;
    var x = statement.x;
    if(op === "add") {
      value += x;
    } else if(op === "div") {
      value /= x;
    } else {
      throw new Error("Unknown operation " + op);
    }
  }
  return value;
}
var program = [
  add(10),
  div(3)
];
var result = run(0, program);
console.log(result); // 3.3333...

Lovers of GoF can argue, they say, "this is Command, not Interpreter." For them, let it be Command. In the context of the article, this is not very important.

In this example, firstly, there is a program consisting of two instructions: “add 10” and “divide by 3”. Whatever it means. Secondly, there is a performer who does something meaningful while looking at the program. It is important to note that the “program” influences the result of its execution very indirectly: the executor is absolutely not obliged to execute instructions from top to bottom, he is not obliged to execute each instruction exactly 1 time, he can generally translate add () calls to “Hello” , and the div () - in "World" .

Let's agree that broadcast add ()in console.log () we are not interested. Computing is interesting. Therefore, we simplify the code a bit by abandoning unnecessary flexibility:

function add(x) { // add(2)(3) === 5
  return function(a) { return a + x; };
}
function div(x) { // div(10)(5) === 2
  return function(a) { return a / x; };
}
function run(value, statements) {
  for(var i = 0; i < statements.length; ++i) {
    var statement = statements[i];
    value = statement(value);
  }
  return value;
}
var program = [ add(10), div(3) ];
var result = run(program);
console.log(0, result); // 3.3333...

It is worth staying here. We have a tool that allows us to describe the program separately and the “way to execute it” separately. Depending on our wishes for the result of the performance, the implementation of the contractor can be very different.

For example, I want that as soon as NaN , null or undefined appears somewhere in the calculations , the calculations stop and the result is null :

...
function run(value, statements) {
  if(!value) {
    return null;
  }
  for(var i = 0; i < statements.length; ++i) {
    var statement = statements[i];
    value = statement(value);
    if(!value) {
      return null;
    }
  }
  return value;
}
console.log(run(undefined, [add(1)])); // null
console.log(run(1, [add(undefined)])); // null

Good. But what if we want to execute the same program for a collection of different initial values? Also not a question:

...
function run(values, statements) {
  return values.map(function(value) {
    for(var i = 0; i < statements.length; ++i) {
      var statement = statements[i];
      value = statement(value);
    }
    return value;
  });
}
var program = [ add(10), div(3) ];
console.log(run([0, 1, 2], program)); // [3.333..., 3.666..., 4]

It is worth staying here again. We use the same expressions to describe the program, but depending on the artist we get very different results. Now let's try to rewrite the example again. This time, firstly, we remove a little more flexibility: expressions are now executed strictly from the first to the last, and secondly, we will get rid of the loop inside run () . We will call the result the word Context (so that no one would guess):

...
function Context(value) {
  this.value = value;
}
Context.prototype.run = function(f) {
  var result = f(this.value);
  return new Context(result);
};
var result = new Context(0)
  .run(add(10))
  .run(div(3))
  .value;
console.log(result); // 3.3333... 

The implementation is very different from the previous options, but it does about the same. It is proposed to introduce the term munada (from the English moonad - "moon advertising"). Hello Identity moonad:

...
function IdentityMoonad(value) {
  this.value = value;
}
IdentityMoonad.prototype.bbind = function(f) {
  var result = f(this.value);
  return new IdentityMoonad(result);
};
var result = new IdentityMoonad(0)
  .bbind(add(10))
  .bbind(div(3))
  .value;
console.log(result); // 3.3333... 

This thing is somewhat remotely similar to Identity monad .

Now let’s recall about the version of the artist where we fought with NaN and try to rewrite it using a new approach to implementation:

function MaybeMoonad(value) {
  this.value = value;
}
MaybeMoonad.prototype.bbind = function(f) {
  if(!this.value) {
    return this;
  }
  var result = f(this.value);
  return new MaybeMoonad(result);
};
var result = new MaybeMoonad(0)
  .bbind(add(10))
  .bbind(add(undefined))
  .bbind(div(3))
  .value;
console.log(result); // null

You can even more familiar example:

var person = {
  // address: {
  //   city: {
  //     name: "New York"
  //   }
  // }
};
console.log(person.address.city.name); // падает
console.log(new MaybeMoonad(person)
  .bbind(function(person) { return person.address; })
  .bbind(function(address) { return address.city; })
  .bbind(function(city) { return city.name; })
  .bbind(function(cityName) { return cityName; })
  .value); // не падает, возвращает null

From afar, it might seem like Maybe monad . The kind reader is invited to independently implement something similar to List monad .

With basic file skills, it doesn't make sense to change IdentityMoonad so that f () calls become asynchronous. The result is a Promise moonad (something similar to q ).

Now, if you look closely at the latest examples, you can try to give a more or less formal definition of munada. A munada is a thing that has 2 operations:
1. return - takes the usual value, puts it in the munadic context and returns that same context. This is just a constructor call.
2. bind - takes a function from the usual value that returns the usual value, executes it in the context of the munadic context and returns the monadic context. This is a call to `bbind ()`.

Also popular now: