Eintrag
Im
YOW! 2013 einer der Entwickler der Haskell-Sprache,
prof. Philip Wadler zeigte, wie Monaden es reinen Funktionssprachen ermöglichen, im Wesentlichen zwingende Operationen wie Eingabe-Ausgabe und Ausnahmebehandlung auszuführen. Es überrascht nicht, dass das Interesse des Publikums an diesem Thema zu einem explosionsartigen Wachstum der Veröffentlichungen über Monaden im Internet geführt hat. Leider verwenden die meisten dieser Veröffentlichungen Beispiele, die in funktionalen Sprachen geschrieben sind, was bedeutet, dass Neulinge in der funktionalen Programmierung etwas über Monaden lernen möchten. Monaden sind jedoch nicht spezifisch für Haskell oder funktionale Sprachen und können durch Beispiele in imperativen Programmiersprachen veranschaulicht werden. Dies ist der Zweck dieses Handbuchs.
Wie unterscheidet sich dieser Leitfaden von den anderen? Wir werden versuchen, die Monaden in nicht mehr als 15 Minuten zu öffnen, wobei wir nur die Intuition und einige elementare Beispiele für Python-Code verwenden. Daher werden wir nicht anfangen, zu theoretisieren und uns mit Philosophie zu befassen und
Burritos ,
Raumanzüge ,
Schreibtische und Endofunktoren zu diskutieren.
Motivationsbeispiele
Wir werden drei Fragen im Zusammenhang mit der Zusammensetzung von Funktionen betrachten. Wir werden sie auf zwei Arten lösen: den üblichen Imperativ und die Verwendung von Monaden. Dann vergleichen wir die verschiedenen Ansätze.
1. Protokollierung
Angenommen, wir haben drei unäre Funktionen:
f1
,
f2
und
f3
, die eine Zahl annehmen und diese um 1, 2 bzw. 3 erhöht zurückgeben. Jede Funktion generiert auch eine Nachricht, die einen Bericht über den abgeschlossenen Vorgang darstellt.
def f1(x): return (x + 1, str(x) + "+1") def f2(x): return (x + 2, str(x) + "+2") def f3(x): return (x + 3, str(x) + "+3")
Wir möchten sie verketten, um den Parameter
x
, mit anderen Worten, wir möchten
x+1+2+3
berechnen. Darüber hinaus müssen wir eine lesbare Erklärung erhalten, was jede Funktion getan hat.
Wir können das gewünschte Ergebnis auf folgende Weise erzielen:
log = "Ops:" res, log1 = f1(x) log += log1 + ";" res, log2 = f2(res) log += log2 + ";" res, log3 = f3(res) log += log3 + ";" print(res, log)
Diese Lösung ist nicht ideal, da sie aus einer großen Anzahl monotoner Middleware besteht. Wenn wir unserer Kette eine neue Funktion hinzufügen möchten, müssen wir diesen Verknüpfungscode wiederholen. Darüber hinaus beeinträchtigen Manipulationen mit den Variablen
res
und
log
die Lesbarkeit des Codes, was es schwierig macht, der Hauptlogik des Programms zu folgen.
Idealerweise sollte ein Programm wie eine einfache Funktionskette aussehen, wie
f3(f2(f1(x)))
. Leider stimmen die von
f1
und
f2
Datentypen nicht mit den Parametertypen
f2
und
f3
überein. Aber wir können der Kette neue Funktionen hinzufügen:
def unit(x): return (x, "Ops:") def bind(t, f): res = f(t[0]) return (res[0], t[1] + res[1] + ";")
Jetzt können wir das Problem wie folgt lösen:
print(bind(bind(bind(unit(x), f1), f2), f3))
Das folgende Diagramm zeigt den bei
x=0
ablaufenden Rechenprozess. Hier sind
v1
,
v2
und
v3
die Werte, die als Ergebnis von Aufrufen von
unit
und
bind
.

Die
unit
konvertiert den Eingabeparameter
x
in ein Tupel aus einer Zahl und einer Zeichenfolge. Die
bind
ruft die an sie übergebene Funktion als Parameter auf und akkumuliert das Ergebnis in der Zwischenvariablen
t
.
Wir konnten vermeiden, Middleware zu wiederholen, indem wir sie in die
bind
einfügten. Wenn wir nun die Funktion
f4
,
f4
wir sie einfach in die Kette ein:
bind(f4, bind(f3, ... ))
Und wir müssen keine weiteren Änderungen vornehmen.
2. Liste der Zwischenwerte
Wir werden dieses Beispiel auch mit einfachen unären Funktionen beginnen.
def f1(x): return x + 1 def f2(x): return x + 2 def f3(x): return x + 3
Wie im vorherigen Beispiel müssen wir diese Funktionen zusammensetzen, um
x+1+2+3
zu berechnen. Wir müssen auch eine Liste aller Werte erhalten, die sich aus der Arbeit unserer Funktionen ergeben, dh
x
,
x+1
,
x+1+2
und
x+1+2+3
.
Im Gegensatz zum vorherigen Beispiel sind unsere Funktionen zusammensetzbar, dh die Typen ihrer Eingabeparameter stimmen mit dem Typ des Ergebnisses überein. Daher gibt eine einfache Kette
f3(f2(f1(x)))
das Endergebnis zurück. In diesem Fall verlieren wir jedoch die Zwischenwerte.
Lösen wir das Problem "frontal":
lst = [x] res = f1(x) lst.append(res) res = f2(res) lst.append(res) res = f3(res) lst.append(res) print(res, lst)
Leider enthält diese Lösung auch viel Middleware. Und wenn wir uns entscheiden,
f4
hinzuzufügen, müssen wir diesen Code erneut wiederholen, um die richtige Liste der Zwischenwerte zu erhalten.
Daher fügen wir wie im vorherigen Beispiel zwei zusätzliche Funktionen hinzu:
def unit(x): return (x, [x]) def bind(t, f): res = f(t[0]) return (res, t[1] + [res])
Jetzt schreiben wir das Programm als eine Kette von Aufrufen um:
print(bind(bind(bind(unit(x), f1), f2), f3))
Das folgende Diagramm zeigt den bei
x=0
ablaufenden Rechenprozess. Wiederum bezeichnen
v1
,
v2
und
v3
die Werte, die von
unit
und
bind
werden.

3. Leere Werte
Versuchen wir, ein interessanteres Beispiel zu geben, diesmal mit Klassen und Objekten. Angenommen, wir haben eine
Employee
Klasse mit zwei Methoden:
class Employee: def get_boss(self):
Jedes Objekt der
Employee
Klasse verfügt über einen Manager (ein anderes Objekt der
Employee
Klasse) und ein Gehalt, auf die über geeignete Methoden zugegriffen wird. Beide Methoden können auch
None
(der Mitarbeiter hat keinen Manager, das Gehalt ist unbekannt).
In diesem Beispiel erstellen wir ein Programm, das das Gehalt des Leiters dieses Mitarbeiters anzeigt. Wenn der Manager nicht gefunden wird oder sein Gehalt nicht ermittelt werden kann, gibt das Programm
None
.
Im Idealfall müssen wir so etwas schreiben
print(john.get_boss().get_wage())
In diesem Fall wird unser Programm jedoch mit einem Fehler beendet, wenn eine der Methoden
None
zurückgibt.
Ein offensichtlicher Weg, um mit dieser Situation umzugehen, könnte folgendermaßen aussehen:
result = None if john is not None and john.get_boss() is not None and john.get_boss().get_wage() is not None: result = john.get_boss().get_wage() print(result)
In diesem Fall erlauben wir zusätzliche Aufrufe der
get_wage
get_boss
und
get_wage
. Wenn diese Methoden schwer genug sind (z. B. Zugriff auf die Datenbank), reicht unsere Lösung nicht aus. Deshalb ändern wir es:
result = None if john is not None: boss = john.get_boss() if boss is not None: wage = boss.get_wage() if wage is not None: result = wage print(result)
Dieser Code ist hinsichtlich der Berechnung optimal, wird jedoch aufgrund von drei verschachtelten
if
schlecht gelesen. Daher werden wir versuchen, den gleichen Trick wie in den vorherigen Beispielen zu verwenden. Definieren Sie zwei Funktionen:
def unit(e): return e def bind(e, f): return None if e is None else f(e)
Und jetzt können wir die gesamte Lösung in einer Zeile zusammenfassen.
print(bind(bind(unit(john), Employee.get_boss), Employee.get_wage))
Wie Sie wahrscheinlich bereits bemerkt haben, mussten wir in diesem Fall die
unit
nicht schreiben: Sie gibt einfach den Eingabeparameter zurück. Aber wir werden es so lassen, dass es für uns einfacher ist, unsere Erfahrungen zu verallgemeinern.
Beachten Sie auch, dass wir in Python Methoden als Funktionen verwenden können, mit denen wir
Employee.get_boss(john)
anstelle von
john.get_boss()
schreiben konnten.
Das folgende Diagramm zeigt den Berechnungsprozess, wenn John keinen Anführer hat, d. H.
john.get_boss()
gibt
None
.

Schlussfolgerungen
Angenommen, wir möchten die Funktionen des gleichen Typs
f1
,
f2
,
…
,
fn
kombinieren. Wenn ihre Eingabeparameter mit den Ergebnissen übereinstimmen, können wir eine einfache Kette der Form
fn(… f2(f1(x)) …)
. Das folgende Diagramm zeigt einen verallgemeinerten Berechnungsprozess mit Zwischenergebnissen, die als
v1
,
v2
,
…
,
vn
.

Oft ist dieser Ansatz nicht anwendbar. Die Arten von Eingabewerten und Funktionsergebnissen können wie im ersten Beispiel variieren. Oder Funktionen können zusammensetzbar sein, aber wir möchten zusätzliche Logik zwischen Aufrufen einfügen, da wir in den Beispielen 2 und 3 eine Aggregation von Zwischenwerten bzw. eine Prüfung auf einen leeren Wert eingefügt haben.
1. Imperative Entscheidung
In allen Beispielen haben wir zunächst den einfachsten Ansatz verwendet, der durch das folgende Diagramm dargestellt werden kann:

Bevor wir
f1
aufrufen
f1
wir einige Initialisierungen durchgeführt. Im ersten Beispiel haben wir eine Variable zum Speichern eines gemeinsamen Protokolls initialisiert, im zweiten für eine Liste von Zwischenwerten. Danach haben wir Funktionsaufrufe mit einem bestimmten Verbindungscode durchsetzt: Wir haben Aggregatwerte berechnet und das Ergebnis auf
None
überprüft.
2. Monaden
Wie wir in den Beispielen gesehen haben, litten imperative Entscheidungen immer unter Ausführlichkeit, Wiederholung und verwirrender Logik. Um eleganteren Code zu erhalten, haben wir ein bestimmtes Entwurfsmuster verwendet, nach dem wir zwei Funktionen erstellt haben:
unit
und
bind
. Diese Vorlage wird als
Monade bezeichnet . Die
bind
enthält Middleware, während das
unit
die Initialisierung implementiert. Dies ermöglicht es uns, die endgültige Lösung auf eine Zeile zu vereinfachen:
bind(bind( ... bind(bind(unit(x), f1), f2) ... fn-1), fn)
Der Berechnungsprozess kann durch ein Diagramm dargestellt werden:

Ein Aufruf von
unit(x)
erzeugt einen Anfangswert von
v1
. Dann erzeugt
bind(v1, f1)
einen neuen Zwischenwert
v2
, der beim nächsten Aufruf zum
bind(v2, f2)
. Dieser Prozess wird fortgesetzt, bis ein Endergebnis erhalten wird. Durch die Definition verschiedener
unit
und
bind
im Rahmen dieser Vorlage können wir verschiedene Funktionen in einer Berechnungskette kombinieren.
Monadenbibliotheken (
z. B. PyMonad oder OSlash, ca. Transl. ) Enthalten normalerweise gebrauchsfertige Monaden (Paare von
unit
und
bind
) zum Implementieren bestimmter Funktionszusammensetzungen.
Um Funktionen zu verketten, müssen die von
unit
und
bind
zurückgegebenen Werte vom gleichen Typ sein wie die Eingabeparameter von
bind
. Dieser Typ wird
monadisch genannt . In Bezug auf das obige Diagramm muss der Typ der Variablen
v1
,
v2
,
…
,
vn
ein monadischer Typ sein.
Überlegen Sie abschließend, wie Sie den mit Monaden geschriebenen Code verbessern können. Offensichtlich wirken wiederholte
bind
unelegant. Um dies zu vermeiden, definieren Sie eine andere externe Funktion:
def pipeline(e, *functions): for f in functions: e = bind(e, f) return e
Jetzt stattdessen
bind(bind(bind(bind(unit(x), f1), f2), f3), f4)
Wir können die folgende Abkürzung verwenden:
pipeline(unit(x), f1, f2, f3, f4)
Fazit
Monaden sind ein einfaches und leistungsstarkes Entwurfsmuster, mit dem Funktionen erstellt werden. In deklarativen Programmiersprachen hilft es, zwingende Mechanismen wie Protokollierung oder Eingabe / Ausgabe zu implementieren. In imperativen Sprachen
Es hilft, Code zu verallgemeinern und zu verkürzen, der eine Reihe von Aufrufen von Funktionen desselben Typs verknüpft.
Dieser Artikel bietet nur ein oberflächliches, intuitives Verständnis von Monaden. Weitere Informationen erhalten Sie bei folgenden Quellen:
- Wikipedia
- Monaden in Python (mit schöner Syntax!)
- Zeitleiste der Monaden-Tutorials