So etwas wie Monaden aus der Ferne

Nachdem ich Dutzende von „verständlichsten Einführungen in Monaden“ gelesen und (auch) Dutzende von Diskussionen in verschiedenen Foren gelesen hatte, kam ich zu dem Schluss, dass es eine Gruppe von abstrakten OO-Programmierern gibt, denen meine Interpretation von „so etwas wie Monaden“ dabei helfen kann, der richtigen ein wenig näher zu kommen Verständnis.

In dieser Publikation finden Sie daher keine Antworten auf die folgenden Fragen:
1. Was ist eine Monade?
2. Wo und wie benutzt man Monaden?
3. Warum sind Monaden besser als ihre Abwesenheit?

In der Programmierung gibt es ein solches Phänomen - "Design Patterns". Offiziell ist dies eine Reihe von Best Practices, die bei der Lösung "typischer Probleme" befolgt werden sollten. Inoffiziell sind es nur ein paar Krücken für Sprachen, die keine integrierten Tools zur Lösung gängiger Probleme haben.

Es gibt ein solches Entwurfsmuster - Interpreter . Erstens ist es bemerkenswert, weil Sie auf diese Weise eine Art virtuelle Maschine auf Ihre bevorzugte Programmiersprache aufbauen können, während:

1. Sie können das Programm in einer Sprache beschreiben, die die virtuelle Maschine versteht.
2. Es ist möglich , die Corovans in allen Details auszurauben, um zu beschreiben, wie die virtuelle Maschine die einzelnen Anweisungen interpretieren soll.

Alles, was unten geschrieben ist, ist nur sinnvoll, wenn der freundliche Leser mit dem erwähnten Muster nur minimal vertraut ist.

Ein vergleichsweise kanonisches Beispiel:
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...

Liebhaber von GoF können argumentieren: "Dies ist ein Befehl, kein Dolmetscher." Für sie sei es Befehl. Im Kontext des Artikels ist dies nicht sehr wichtig.

In diesem Beispiel gibt es zunächst ein Programm, das aus zwei Befehlen besteht: "addiere 10" und "dividiere durch 3". Was auch immer es bedeutet. Zweitens gibt es einen Darsteller, der beim Betrachten des Programms etwas Sinnvolles tut . Es ist wichtig zu beachten, dass das "Programm" das Ergebnis seiner Ausführung sehr indirekt beeinflusst: Der Ausführende ist absolut nicht verpflichtet, Anweisungen von oben nach unten auszuführen, er ist nicht verpflichtet, jede Anweisung genau einmal auszuführen, er kann add () -Aufrufe nach "Hello" und div übersetzen () - in "Welt" .

Stimmen wir zu, dass broadcast add ()In console.log () sind wir nicht interessiert. Computing ist interessant. Deshalb vereinfachen wir den Code ein wenig, indem wir auf unnötige Flexibilität verzichten:

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...

Es lohnt sich, hier zu bleiben. Wir haben ein Tool, mit dem wir das Programm separat beschreiben und die Art und Weise, wie es ausgeführt wird, separat beschreiben können. Abhängig von unseren Wünschen nach dem Ergebnis der Leistung kann die Ausführung des Auftragnehmers sehr unterschiedlich sein.

Ich möchte zum Beispiel, dass, sobald NaN , null oder undefined irgendwo in den Berechnungen erscheint , die Berechnungen gestoppt werden und das Ergebnis null ist :

...
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

Gut Was aber, wenn wir dasselbe Programm für eine Sammlung unterschiedlicher Anfangswerte ausführen wollen? Auch keine Frage:

...
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]

Es lohnt sich wieder hier zu übernachten. Wir verwenden die gleichen Ausdrücke, um das Programm zu beschreiben, aber je nach Künstler erhalten wir sehr unterschiedliche Ergebnisse. Versuchen wir nun, das Beispiel erneut zu schreiben. Diesmal entfernen wir erstens etwas mehr Flexibilität: Ausdrücke werden jetzt streng vom ersten bis zum letzten ausgeführt, und zweitens wird die Schleife in run () entfernt . Wir werden das Ergebnis als Kontext bezeichnen (damit niemand es erraten kann):

...
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... 

Die Implementierung unterscheidet sich stark von den vorherigen Optionen, funktioniert jedoch in etwa genauso. Es wird vorgeschlagen, den Begriff Munada (aus dem Englischen Moonad - "Moon Advertising") einzuführen . Hallo Identität 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... 

Dieses Ding ist der Identitätsmonade in gewisser Weise entfernt ähnlich .

Erinnern wir uns jetzt an die Version des Künstlers, in der wir mit NaN gekämpft haben, und versuchen, sie mit einem neuen Ansatz für die Implementierung umzuschreiben:

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

Sie können noch vertrauteres Beispiel:

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

Aus der Ferne scheint es wie vielleicht Monade . Der freundliche Leser ist eingeladen, selbständig etwas Ähnliches wie List monad zu implementieren .

Bei grundlegenden Datei-Kenntnissen ist es nicht sinnvoll , IdentityMoonad so zu ändern , dass f () -Aufrufe asynchron werden. Das Ergebnis ist ein Promise Moonad (ähnlich wie q ).

Wenn Sie sich nun die neuesten Beispiele genauer ansehen, können Sie versuchen, eine mehr oder weniger formale Definition von Munada zu geben. Eine Munada hat zwei Operationen:
1. return - nimmt den üblichen Wert, stellt ihn in den munadischen Kontext und gibt denselben Kontext zurück. Dies ist nur ein Konstruktoraufruf.
2. bind - Nimmt eine Funktion vom normalen Wert, die den normalen Wert zurückgibt, führt sie im Kontext des munadischen Kontexts aus und gibt den monadischen Kontext zurück. Dies ist ein Aufruf von `bbind ()`.

Jetzt auch beliebt: