Da wir für Monaden sowohl eine Infixnotation von bind als auch eine Syntax für Lambda-Ausdrücke brauchen, bietet sich Javascript als weitere Sprache an.
Eine oft vertretene Meinung ist diese, dass JQuery eine monadische API hat und deswegen besonders einfach zu nutzen sei. Es gibt Argumente, dass JQuery nicht in dem Maße monadisch ist.
In diesem Beispiel-Code, der JQuery verwendet,
1 2 3 4 5 | $("button").click(function(){
$("li").each(function(){
alert($(this).text())
});
});
|
wird für jedes p-Element ein Klick-Event-Handler gesetzt, der auf dem jeweiligen Element bestimmte Eigenschaften setzt. Die dazugehörigen Typen der Funktionen sehen in etwa so aus (ungeachtet der Nebeneffekte):
1 2 3 4 5 6 | type Document = Element
$ :: Document -> Query -> [Element]
click :: [Element] -> (Element -> ()) -> ()
each :: [Element] -> (Element -> ()) -> ()
hide :: [Element] -> ()
|
Hier gibt es Anlehnungen an fmap. Ready und Click liften den Event-Handler in den Listenfunktor. Ein impliziter Parameter von $ ist das Dokument. Da das Dokument auch alle Elemente umfasst, ist $ ähnlich eines Filters oder Selektors über allen Elementen. In diesem Beispiel wird keine allgemein gültige Funktion benötigt, die eine ähnliche Anwendung besitzt wie bind.
Als Beispiel wird hier daher eine Implementierung von Monaden vorgestellt, die sich sehr an die Implementierungen aus dem vorherigen Kapiteln anlehnt.
Monaden bestehen auch in Javascript aus diesen beiden Funktionen:
1 2 | function unit(value);
function bind(monad, function(value));
|
Eine Monade ist normalerweise ein JS-Objekt. Für einige Monaden würde es sich anbieten, eine Monade als etwas Anderes zu definieren, allerdings eignet sich ein JS-Objekt auf Grund der Infixnotation von bind für Monaden.
Wie in anderen Sprachen auch, ist unit ein Konstruktor, der einen Wert bekommt und die Monade zurückgibt.
Die Notation der Monadengesetze ist in Javascript ähnlich zu der in Ruby:
1 2 3 4 5 | unit(value).bind(f) === f(value)
monad.bind(unit) === monad
monad.bind(f).bind(g) === monad.bind(function (value) {
return f(value).bind(g)
})
|
Im Unterschied zu Ruby gibt es eine etwas einfachere Variante der Lambdafunktionen. Javascript wandelt im Vergleichsoperator einige Typen implizit um. Hier muss daher der === Operator verwendet werden, um auch die Typen sinnvoll vergleichen zu können.
Wie in C# und Ruby dient die Maybe-Monade als Beispiel für den grundlegenden Aufbau von Monaden.
In Javascript können wir einen Typen definieren, indem dem Typnamen eine Funktion zugewiesen wird, die auf dem This-Objekt arbeitet. In der unit-Funktion wird ein neues Javascript-Objekt instanziiert und als Funktionswert zurückgegeben.
1 2 3 4 5 6 7 | var Maybe = function(value) {
this.value = value;
};
Maybe.unit = function(value) {
return new Maybe(value)
};
|
Javascript verwendet eine Vererbung mithilfe von Prototypen. Falls eine Funktion nicht gefunden wird, wird eine entsprechende Funktion mit diesem Namen in Typ.prototype gesucht. Hier wird bind auf den Prototypen gesetzt. Jedes Maybe-Objekt hat daher die bind-Funktion. In dieser Implementierung wird der Member value auf null geprüft. Dies ist insofern problematisch, als dass es dadurch nicht möglich ist, den Wert Just null von Nothing zu unterscheiden, allerdings kann dann auf die explizite Implementierung von Nothing verzichtet werden.
1 2 3 4 5 | Maybe.prototype.bind = function(fn) {
if (this.value != null)
return fn(this.value);
return this;
};
|
Ret liefert genau den Wert zurück, der im Konstruktor zuvor gesetzt wurde. Wie bind wird ret direkt auf dem Prototypen gesetzt und sieht somit allen Instanzen zur Verfügung.
1 2 3 | Maybe.prototype.ret = function() {
return this.value;
};
|
Um die eben definierte Maybe-Monade in Javascript zu verwenden, wird zuerst ein neues JS-Objekt erzeugt.
1 2 3 4 5 | m1 = Maybe.unit(1);
var maybe11 = m1.bind(function(v) {
return Maybe.unit(v + 10)
})
|
Anschließend wird der Lambda-Ausdruck (in Haskell-Notation)
1 | \ v -> v + 10
|
an den erzeugten monadischen Wert gebunden. Hier fällt auf, dass Lambda-Ausdrücke relativ aufwendig zu schreiben sind: Sie benötigen das Schlüsselwort function und müssen den Rückgabewert explizit mit return zurückgeben. Außerdem fällt auf, dass der Ausdruck mit "})" endet. Das Ergebnis ist in diesem Fall wie erwartet 11.
In einem leicht abgewandelten Beispiel wird ein Nothing-Wert erzeugt:
1 2 3 4 5 | mNull = Maybe.unit(null);
var maybe11 = mNull.bind(function(v) {
return Maybe.unit(v + 10)
})
|
In diesem Fall wird der "Null is not an Object"-Error abgefangen.
Als Beispiel für den Nutzen von Monaden in Javascript sollen hier die Funktionen liftM und liftM2 dienen. LiftM hat in Haskell den Typ:
1 | liftM :: Monad m => (a -> b) -> m a -> m b
|
Sie hebt eine Funktion in die monadische Variante. Hier ist die Implementierung aus der Standardbibliothek:
1 2 3 | liftM f m1 = do
x1 <- m1
return (f x1)
|
Eine entsprechende Implementierung in Javascript sieht so aus:
1 2 3 4 5 6 7 | Maybe.liftM = function(f) {
return function(m1) {
return m1.bind(function(x1) {
return Maybe.unit(f(x1));
})
};
};
|
Hier gibt es einige Auffälligkeiten: Zum Einen wird explizit Maybe verwendet, was einer generellen Verwendbarkeit für alle Monaden widerspricht. Da Javascript im Gegensatz zu Haskell den Typ von unit in Zeile 4 nicht automatisch bestimmen kann, müssen wir explizit eine Monade angeben. Eine Alternative wäre die Spezifizierung einer Monade als Parameter, also die Übergabe eines Prototypen für liftM. Interessanterweise ist diese Vorgehensweise ähnlich zu der, die der Haskell-Compiler intern verwendet, um die konkrete Funktion unit zu erhalten. Zweitens hat jede erzeugte Funktion nur einen Parameter und gibt gegebenenfalls eine Funktion zurück, die wieder genau einen Paramerter hat. So eine Vorgehensweise nennt sich currying, was in Haskell automatisch vom Compiler vorgenommen wird.
Gegeben sei eine Funktion addOne mit dieser Definition:
1 2 3 | var addOne = function(val) {
return val + 1;
};
|
AddOne kann dann in die Maybe-Monade gehoben werden.
1 2 3 4 5 6 | var maybeAddOne = Maybe.liftM(addOne);
m1 = Maybe.unit(1);
mNothing = Maybe.unit(undefined)
maybeAddOne(m1).ret() // == 2
maybeAddOne(mNothing) // == undefined
|
Der Typ in Haskell-Notation ist dann:
1 | maybeAddOne :: Maybe Int -> Maybe Int
|
Eine Implementierung von liftM2 in Javascript ist ähnlich zu der von liftM, mit dem Unteschied, dass die Funktion zwei Parameter hat.
1 2 3 4 5 6 7 8 9 | Maybe.liftM2 = function(fn) {
return function(M1, M2) {
return M1.bind(function(val1) {
return M2.bind(function(val2) {
return Maybe.unit(fn(val1, val2));
});
});
};
};
|
Durch die tief ineinander verschachtelten Lambda-Ausdrücke, wird die Funktion sehr unübersichtlich. Auch wenn die Einrückung syntaktisch korrekt ist, könnte es sinnvoll sein, die bind-Ausdrücke direkt untereinander zu schreiben:
1 2 3 4 5 6 7 | Maybe.liftM2 = function(f) {
return function(M1, M2) {
return M1.bind(function(val1) {
return M2.bind(function(val2) {
return Maybe.unit(f(val1, val2));
});});};
};
|
Bei dieser Schreibweise muss allerdings die Zeile 6 so hingenommen werden. Zwar gibt es einen Zusammenhang zwischen einem bind-Aufruf und einem Vorkommen von "});", allerdings ist dieser nicht offensichtlich. Die entsprechende Implementierung in Haskell mit der Do-Notation beinhaltet deutlich weniger Sonderzeichen:
1 2 3 4 5 | liftM2 :: (Monad m) => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
liftM2 f m1 m2 = do
val1 <- m1
val2 <- m2
return (f val1 val2)
|
Seien add, m2 und mNothing wie folgt definiert: add ist eine Funktion, die zwei Zahlen addiert. M2 und mNothing sind beides monadische Werte.
1 2 3 | var add = function(a, b) {return a + b;};
m2 = Maybe.unit(2);
mNothing = Maybe.unit(undefined);
|
LiftM2Add ist dann wie maybeAddOne eine in die Maybe-Monade gehobene Funktion von add.
1 2 3 4 5 | var liftM2Add = Maybe.liftM2(add);
var three = liftM2Add(m1, m2).ret(); // == 3
var undef = liftM2Add(m3, m2); // == mNothing
var undef2 = liftM2Add(m1, m3); // == mNothing
|