
Auf Habré finden Sie viele Veröffentlichungen, die sowohl die Theorie der Monaden als auch die Praxis ihrer Anwendung offenbaren. Die meisten dieser Artikel werden über Haskell erwartet. Ich werde die Theorie nicht zum n-ten Mal nacherzählen. Heute werden wir über einige Erlang-Probleme sprechen, Möglichkeiten, sie mit Monaden zu lösen, teilweise Verwendung von Funktionen und syntaktischen Zucker von erlando - eine coole Bibliothek des RabbitMQ-Teams.
Einführung
Erlang hat Unveränderlichkeit, aber keine Monaden * . Dank der parse_transform-Funktionalität und der erlando-Implementierung in der Sprache besteht jedoch weiterhin die Möglichkeit, Monaden in Erlang zu verwenden.
Über Immunität am Anfang der Geschichte habe ich nicht zufällig gesprochen. Immunität ist fast überall und immer - eine der Hauptideen von Erlang. Durch Immunität und Reinheit der Funktionen können Sie sich auf die Entwicklung einer bestimmten Funktion konzentrieren und haben keine Angst vor Nebenwirkungen. Neulinge in Erlang, die beispielsweise aus Java oder Python kommen, finden es jedoch ziemlich schwierig, Erlangs Ideen zu verstehen und zu akzeptieren. Besonders wenn Sie sich an die Erlang-Syntax erinnern. Diejenigen, die versuchten, Erlang zu verwenden, bemerkten wahrscheinlich seine Ungewöhnlichkeit und Unabhängigkeit. Auf jeden Fall habe ich viele Rückmeldungen von Anfängern gesammelt und die „seltsame“ Syntax führt die Bewertung an.
Erlando
Erlando ist ein Erlang-Erweiterungsset, das uns Folgendes bietet:
- Teilweise Verwendung / Currying von Funktionen mit schemaartigen Schnitten
- Haskell-ähnliche Notationen
- import-as - syntaktischer Zucker zum Importieren von Funktionen aus anderen Modulen.
Hinweis: Ich habe die folgenden Codebeispiele verwendet, um die Merkmale von erlando aus Matthew Sackmans Präsentation zu veranschaulichen, und sie teilweise mit meinem Code und meinen Erklärungen verdünnt.
Abstrakter Schnitt
Gehen Sie direkt zum Punkt. Betrachten Sie mehrere Funktionen eines realen Projekts:
info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties{ expiry = calculate_msg_expiry(TTL)} end.
Alle diese Funktionen dienen dazu, Parameter durch einfache Ausdrücke zu ersetzen. Tatsächlich handelt es sich hierbei um eine Teilanwendung, da einige Parameter vor dem Aufruf nicht bekannt sind. Zusammen mit der Flexibilität fügen diese Funktionen unserem Code Rauschen hinzu. Durch ein wenig Ändern der Syntax - durch Eingabe von cut - können Sie die Situation verbessern.
Wert _
- _ kann in Vorlagen verwendet werden
- Mit Ausschneiden können Sie _ Außenmuster verwenden
- Wenn es sich außerhalb der Vorlage befindet, wird es zu einem Parameter für den Ausdruck, in dem es sich befindet
- Die mehrfache Verwendung von _ innerhalb desselben Ausdrucks führt zur Ersetzung mehrerer Parameter in diesem Ausdruck
- Cut ist kein Ersatz für Verschlüsse (Funs)
- Argumente werden vor der Schnittfunktion ausgewertet.
Cut verwendet _ in Ausdrücken, um anzugeben, wo die Abstraktion angewendet werden soll. Ausschneiden umschließt nur die nächstgelegene Ebene im Ausdruck, verschachtelter Schnitt ist jedoch nicht verboten.
Zum Beispiel list_to_binary([1, 2, math:pow(2, _)]).
list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]).
zu list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]).
aber nicht im fun (X) -> list_to_binary([1, 2, math:pow(2, X)]) end.
.
Es klingt etwas verwirrend. Schreiben wir die obigen Beispiele mit cut neu:
info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). info_all(VHostPath, Items) -> map(VHostPath, info(_, Items)).
backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). backing_queue_timeout(State = #q{backing_queue = BQ}) -> run_backing_queue(BQ, _:timeout(_), State).
reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties { expiry = calculate_msg_expiry(TTL) } end. reset_msg_expiry_fun(TTL) -> _ #message_properties { expiry = calculate_msg_expiry(TTL) }.
Reihenfolge der Argumentberechnung
Betrachten Sie das folgende Beispiel, um die Reihenfolge zu veranschaulichen, in der die Argumente berechnet werden:
f1(_, _) -> io:format("in f1~n"). test() -> F = f1(io:format("test line 1~n"), _), F(io:format("test line 2~n")).
Da die Argumente vor der Schnittfunktion ausgewertet werden, wird Folgendes angezeigt:
test line 2 test line 1 in f1
Schneiden Sie die Abstraktion in verschiedene Arten und Muster von Code
- Tupel
F = {_, 3}, {a, 3} = F(a).
- Listen
dbl_cons(List) -> [_, _ | List]. test() -> F = dbl_cons([33]), [7, 8, 33] = F(7, 8).
- Aufzeichnungen
-record(vector, { x, y, z }). test() -> GetZ = _#vector.z, 7 = GetZ(#vector { z = 7 }), SetX = _#vector{x = _}, V = #vector{ x = 5, y = 4 } = SetX(#vector{ y = 4 }, 5).
- Fälle
F = case _ of N when is_integer(N) -> N + N; N -> N end, 10 = F(5), ok = F(ok).
- Karten
test() -> GetZ = maps:get(z, _), 7 = GetZ(#{ z => 7 }), SetX = _#{x => _}, V = #{ x := 5, y := 4 } = SetX(#{ y => 4 }, 5).
- Übereinstimmende Listen und Erstellen von Binärdaten
test_cut_comprehensions() -> F = << <<(1 + (X*2))>> || _ <- _, X <- _ >>,
Vorteile
- Der Code ist kleiner geworden, daher ist die Wartung einfacher.
- Der Code ist einfacher und übersichtlicher geworden.
- Lärm vom Spaß gegangen.
- Für Anfänger in Erlang ist es bequemer, Get / Set-Funktionen zu schreiben.
Nachteile
- Erhöhte Einstiegsschwelle für erfahrene Erlang-Entwickler und reduzierte Einstiegsschwelle für Anfänger. Jetzt muss das Team den Schnitt verstehen und eine weitere Syntax kennen.
Notation machen
Weiches Komma ist ein rechnerisch bindendes Konstrukt. Erlang hat kein faules Berechnungsmodell. Stellen wir uns vor, was passieren würde, wenn Erlang faul wie Haskell wäre
my_function() -> A = foo(), B = bar(A, dog), ok.
Um die Ausführungsreihenfolge zu gewährleisten, müssten wir die Berechnungen explizit durch Definieren eines Kommas verknüpfen.
my_function() -> A = foo(), comma(), B = bar(A, dog), comma(), ok.
Setzen Sie die Konvertierung fort:
my_function() -> comma(foo(), fun (A) -> comma(bar(A, dog), fun (B) -> ok end)).
Basierend auf der Schlussfolgerung ist Komma / 2 eine idiomatische Funktion >>=/2
. Die Monade benötigt nur drei Funktionen: >>=/2
, return/1
und fail/1
.
Alles wäre in Ordnung, aber die Syntax ist einfach schrecklich. Wir wenden erlando
von erlando
.
do([Monad || A <- foo(), B <- bar(A, dog), ok]).
Arten von Monaden
Da der do-Block parametrisiert ist, können wir Monaden verschiedener Typen verwenden. Innerhalb des Do-Blocks werden die Aufrufe return/1
und fail/1
für Monad:return/1
bzw. Monad:fail/1
bereitgestellt.
Identitätsmonade.
Die identische Monade ist die einfachste Monade, die den Wertetyp nicht ändert und nicht an der Steuerung des Berechnungsprozesses beteiligt ist. Es wird mit Transformatoren angewendet. Führt Verknüpfungsausdrücke aus - das oben beschriebene Software-Komma.
Vielleicht-Monade.
Monade von Berechnungen mit Verarbeitung fehlender Werte. Das Verknüpfen eines Parameters mit einer parametrisierten Berechnung ist die Übertragung eines Parameters auf eine Berechnung. Das Verknüpfen eines fehlenden Parameters mit einer parametrisierten Berechnung ist ein fehlendes Ergebnis.
Betrachten Sie ein Beispiel für vielleicht_m:
if_safe_div_zero(X, Y, Fun) -> do([maybe_m || Result <- case Y == 0 of true -> fail("Cannot divide by zero"); false -> return(X / Y) end, return(Fun(Result))]).
Die Auswertung des Ausdrucks wird beendet, wenn nichts zurückgegeben wird.
{just, 6} = if_safe_div_zero(10, 5, _+4) ## 10/5 = 2 -> 2+4 -> 6 nothing = if_safe_div_zero(10, 0, _+4)
Fehlermonade.
Ähnlich wie vielleicht_m, nur mit Fehlerbehandlung. Manchmal gilt das Let it Crash-Prinzip nicht und Fehler müssen zum Zeitpunkt ihres Auftretens behandelt werden. In diesem Fall erscheinen häufig Treppen aus dem Fall im Code, zum Beispiel diese:
write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], case make_binary(Data) of Bin when is_binary(Bin) -> case file:open(Path, Modes1) of {ok, Hdl} -> case file:write(Hdl, Bin) of ok -> case file:sync(Hdl) of ok -> file:close(Hdl); {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> E end; {error, _} = E -> E end.
make_binary(Bin) when is_binary(Bin) -> Bin; make_binary(List) -> try iolist_to_binary(List) catch error:Reason -> {error, Reason} end.
Das zu lesen ist unangenehm, sieht aus wie Rückrufnudeln in JS. Error_m kommt zur Rettung:
write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], do([error_m || Bin <- make_binary(Data), Hdl <- file:open(Path, Modes1), Result <- return(do([error_m || file:write(Hdl, Bin), file:sync(Hdl)])), file:close(Hdl), Result]). make_binary(Bin) when is_binary(Bin) -> error_m:return(Bin); make_binary(List) -> try error_m:return(iolist_to_binary(List)) catch error:Reason -> error_m:fail(Reason) end.
- Listenmonade.
Werte sind Listen, die als mehrere mögliche Ergebnisse einer einzelnen Berechnung interpretiert werden können. Wenn eine Berechnung von einer anderen abhängt, wird die zweite Berechnung für jedes Ergebnis der ersten durchgeführt, und die Ergebnisse (zweite Berechnung) werden in einer Liste gesammelt.
Betrachten Sie das Beispiel mit den klassischen pythagoreischen Tripeln. Wir berechnen sie ohne Monaden:
P = [{X, Y, Z} || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)].
Gleiches gilt nur für list_m:
P = do([list_m || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), monad_plus:guard(list_m, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)), return({X,Y,Z})]).
- Staatsmonade.
Monade des Stateful Computing.
Ganz am Anfang des Artikels haben wir über die Schwierigkeiten von Anfängern bei der Arbeit mit einem variablen Zustand gesprochen. Oft sieht der Code ungefähr so aus:
State1 = init(Dimensions), State2 = plant_seeds(SeedCount, State1), {DidFlood, State3} = pour_on_water(WaterVolume, State2), State4 = apply_sunlight(Time, State3), {DidFlood2, State5} = pour_on_water(WaterVolume, State4), {Crop, State6} = harvest(State5), ...
Mit einem Transformator und einer Schnittnotation kann dieser Code kompakter und lesbarer umgeschrieben werden:
StateT = state_t:new(identity_m), SM = StateT:modify(_), SMR = StateT:modify_and_return(_), StateT:exec( do([StateT || StateT:put(init(Dimensions)), SM(plant_seeds(SeedCount, _)), DidFlood <- SMR(pour_on_water(WaterVolume, _)), SM(apply_sunlight(Time, _)), DidFlood2 <- SMR(pour_on_water(WaterVolume, _)), Crop <- SMR(harvest(_)), ... ]), undefined).
- Omega-Monade.
Ähnlich wie list_m Monade. Der Durchgang erfolgt jedoch diagonal.
Versteckte Fehlerbehandlung
Wahrscheinlich eines meiner Lieblingsfeatures der error_m
Monade. Unabhängig davon, wo der Fehler auftritt, gibt die Monade immer entweder {ok, Result}
oder {error, Reason}
. Ein Beispiel zur Veranschaulichung des Verhaltens:
do([error_m || Hdl <- file:open(Path, Modes), Data <- file:read(Hdl, BytesToRead), file:write(Hdl, DataToWrite), file:sync(Hdl), file:close(Hdl), file:rename(Path, Path2), file:delete(Path), return(Data)]).
Import_as
Für einen Snack haben wir die Syntax import_as Zucker. Mit der Standardsyntax für das Attribut -import / 2 können Sie Funktionen von anderen in das lokale Modul importieren. Mit dieser Syntax können Sie der importierten Funktion jedoch keinen alternativen Namen zuweisen. Import_as löst dieses Problem:
-import_as({my_mod, [{size/1, m_size}]}) -import_as({my_other_mod, [{size/1, o_size}]})
Diese Ausdrücke werden jeweils zu realen lokalen Funktionen erweitert:
m_size(A) -> my_mod:size(A). o_size(A) -> my_other_mod:size(A).
Fazit
Mit Monaden können Sie den Berechnungsprozess natürlich mit aussagekräftigeren Methoden steuern, Code und Zeit sparen, um ihn zu unterstützen. Auf der anderen Seite erhöhen sie die Komplexität untrainierter Teammitglieder.
* - in der Tat existieren in Erlang Monaden ohne Erlando. Ein Komma, das Ausdrücke trennt, ist eine Konstruktion zum Linearisieren und Verknüpfen von Berechnungen.
PS Vor kurzem wurde die Erlando-Bibliothek von den Autoren als Archiv markiert. Ich habe diesen Artikel vor mehr als einem Jahr geschrieben. Damals wie heute gab es auf Habré jedoch keine Informationen über Monaden in Erlang. Um dieser Situation abzuhelfen, veröffentliche ich diesen Artikel, wenn auch verspätet.
Um erlando in erlang> = 22 zu verwenden, müssen Sie das Problem mit veraltetem erlang beheben: get_stacktrace / 0. Ein Beispiel für einen Fix finden Sie in meiner Gabel: https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2
Danke für deine Zeit!