Codeschnippsel
Terme laden und speichern
12 10 2009
Mit Erlang lassen sich leicht Terme in eine Datei schreiben und wieder laden. Hier ein kleines Beispiel dazu.
-module(term).
-export([lade_term_liste/1, schreibe_term_liste/2]).
lade_term_liste(Dateiname) ->
file:consult(Dateiname).
schreibe_term_liste(Dateiname, Liste) ->
case file:open(Dateiname, write) of
{ok, Handler} ->
lists:foreach(fun(X) -> io:format(Handler, "~p.~n", [X]) end, Liste),
file:close(Handler);
{error, Reason} ->
{error, Reason}
end.
Die Datei "person.dat" besteht aus den folgenden Erlang-Termen:
{person,"Hans",maennlich,1971}.
{person,"Franz",maenlich,1990}.
{person,"Michaela",weiblich,1988}.
{person,"Steffi",weiblich,1978}.
{person,"Boris",maennlich,1969}.
{person,"Petra",weiblich,1968}.
Wir laden in der Shell die Daten, filtern diese und schreiben das Ergebnis in die Datei "zielgruppe.dat":
> {ok, DB} = term:lade_term_liste("person.dat").
> Zielgruppe = [ P || P = {person, _, Geschlecht, Geburtsjahr} <- DB,
Geschlecht == weiblich, Geburtsjahr > 1970].
> term:schreibe_term_liste("zielgruppe.dat", Zielgruppe).
Die Datei "zielgruppe.dat" enthält nun die folgenden Terme:
{person,"Michaela",weiblich,1988}.
{person,"Steffi",weiblich,1978}.
Wirklich einfach, oder?
-module(term).
-export([lade_term_liste/1, schreibe_term_liste/2]).
lade_term_liste(Dateiname) ->
file:consult(Dateiname).
schreibe_term_liste(Dateiname, Liste) ->
case file:open(Dateiname, write) of
{ok, Handler} ->
lists:foreach(fun(X) -> io:format(Handler, "~p.~n", [X]) end, Liste),
file:close(Handler);
{error, Reason} ->
{error, Reason}
end.
Die Datei "person.dat" besteht aus den folgenden Erlang-Termen:
{person,"Hans",maennlich,1971}.
{person,"Franz",maenlich,1990}.
{person,"Michaela",weiblich,1988}.
{person,"Steffi",weiblich,1978}.
{person,"Boris",maennlich,1969}.
{person,"Petra",weiblich,1968}.
Wir laden in der Shell die Daten, filtern diese und schreiben das Ergebnis in die Datei "zielgruppe.dat":
> {ok, DB} = term:lade_term_liste("person.dat").
> Zielgruppe = [ P || P = {person, _, Geschlecht, Geburtsjahr} <- DB,
Geschlecht == weiblich, Geburtsjahr > 1970].
> term:schreibe_term_liste("zielgruppe.dat", Zielgruppe).
Die Datei "zielgruppe.dat" enthält nun die folgenden Terme:
{person,"Michaela",weiblich,1988}.
{person,"Steffi",weiblich,1978}.
Wirklich einfach, oder?
Tautologie Checker
04 10 2009
Ein Tautologie Checker prüft, ob eine gegebene logische Formel für alle Belegungen stets logisch wahr ist. Dieses Beispiel hier basiert auf einem einfachen Tautologie Checker aus dem Buch „ML for the Working Programmer“ von L.C. Paulsen und soll zeigen, wie leicht auch in Erlang Termersetzungen programmiert werden können.
Mit den folgenden Funktionen werden unsere logischen Ausdrücke erzeugt:
atom(Str) -> {atom, Str}.
neg(P) -> {neg, P}.
konj(P, Q) -> {konj, P, Q}.
disj(P, Q) -> {disj, P, Q}.
impl(P, Q) -> disj(neg(P), Q).
Zunächst erzeugen wir drei Attribute „reich“, „gelandet“ und „heilig“:
> Reich = atom ("reich").
> Gelandet = atom ("gelandet").
> Heilig = atom ("heilig").
Zudem gibt es zwei Annahmen über reiche Personen: „Personen, die landen sind reich“ und „Eine Person kann nicht zugleich heilig und reich sein“:
> Annahme1 = impl(Gelandet, Reich).
> Annahme2 = neg(konj(Heilig, Reich)).
Aus diesen beiden Annahmen folgern wir, dass „Personen die landen nicht heilig sind“.
> Folgerung = impl(Gelandet, neg(Heilig)).
Wir wollen somit folgendes Ziel prüfen:
> Ziel = impl(konj(Annahme1, Annahme2), Folgerung).
Der logische Ausdruck „Ziel“ wird zur Prüfung in eine konjunktive Normalform überführt. Die konjunktive Normalform besteht aus Konjunktionen bei denen die einzelnen Glieder der Konjunktionen Disjunktionen sind. In jedem solchen Glied kommt dabei jedes Atom negiert oder nicht negiert vor (link). Zur Transformation definieren wir die Funktionen nnf/1, distrib/2, knf/1. Die Funktion nnf/1 - Negation Normalform - zieht alle Negationen zu den Atomen (De Morgansches Gesetzt). distrib/1 wendet das Distributivgesetz an, knf/1 berechnet die konjunktive Normalform.
% Negationsnormalform (Negation nur auf Ebene der Atome)
nnf({atom, P}) -> atom(P);
nnf({neg, {atom, P}}) -> neg(atom(P));
nnf({neg, {neg, P}}) -> nnf(P);
nnf({neg, {konj, P, Q}}) -> nnf(disj(neg(P), neg(Q)));
nnf({neg, {disj, P, Q}}) -> nnf(konj(neg(P), neg(Q)));
nnf({konj, P, Q}) -> konj(nnf(P), nnf(Q));
nnf({disj, P, Q}) -> disj(nnf(P), nnf(Q)).
% Distributiv Gesetz
distrib(P, {konj, Q, R}) -> konj(distrib(P,Q), distrib(P,R));
distrib({konj, Q, R}, P) -> konj(distrib(Q,P), distrib(R,P));
distrib(P,Q) -> disj(P,Q).
% Konjunktive Normalform
knf({konj, P,Q}) -> konj(knf(P), knf(Q));
knf({disj, P, Q}) -> distrib(knf(P), knf(Q));
knf(P) -> P.
Die eigentliche Tautologie wird geprüft, in dem für jedes Konjunktionsglied geschaut wird, ob dieses zu true ausgewertet werden kann. Um dieses zu machen schauen wir, ob mindestens ein Atom positiv also auch negativ in der Disjunktion vorkommt. Wenn das der Fall ist, wird dieses Konjunktionsglied true. Dieses geschieht mit Hilfe der Funktionen positiv/1 und negativ/1. Diese Funktionen erstellen aus jeder Disjunktion Listen der Vorkommen positiver und negativer Atome. Wir prüfen dann, ob deren Schnittmenge nicht leer ist.
% Alle positiven Atome in den Disjunktion
positiv({atom, P}) -> [P];
positiv({neg, {atom, _}}) -> [];
positiv({disj, P, Q}) -> positiv(P) ++ positiv(Q);
positiv(_) -> throw(keine_normalform).
% Alle negativen Atome in der Disjunktion
negativ({atom, _}) -> [];
negativ({neg, {atom, P}}) -> [P];
negativ({disj, P, Q}) -> negativ(P) ++ negativ(Q);
negativ(_) -> throw(keine_normalform).
ist_tautologie({konj, P,Q}) -> ist_tautologie (P) andalso ist_tautologie (Q);
ist_tautologie(P) ->
Pos = sets:from_list(positiv(P)),
Neg = sets:from_list(negativ(P)),
Result = sets:intersection(Pos, Neg),
sets:size(Result) > 0.
Hier der Funktionsaufruf:
> ist_tautologie(knf(nnf(Ziel))).
true
Damit wird unser Ziel zu true und der Ausdruck ist somit eine Tautologie. Damit sind Personen, die landen, nicht heilig.
Hinweis: Diese Art Tautologie Checker ist in der Praxis kaum praktikabel. Für seriöse Probleme werden andere Codierungen und Lösungsstrategien verwendet, etwa mit Binären Entscheiddiagrammen, kurz BDDs. Siehe auch hier.
Mit den folgenden Funktionen werden unsere logischen Ausdrücke erzeugt:
atom(Str) -> {atom, Str}.
neg(P) -> {neg, P}.
konj(P, Q) -> {konj, P, Q}.
disj(P, Q) -> {disj, P, Q}.
impl(P, Q) -> disj(neg(P), Q).
Zunächst erzeugen wir drei Attribute „reich“, „gelandet“ und „heilig“:
> Reich = atom ("reich").
> Gelandet = atom ("gelandet").
> Heilig = atom ("heilig").
Zudem gibt es zwei Annahmen über reiche Personen: „Personen, die landen sind reich“ und „Eine Person kann nicht zugleich heilig und reich sein“:
> Annahme1 = impl(Gelandet, Reich).
> Annahme2 = neg(konj(Heilig, Reich)).
Aus diesen beiden Annahmen folgern wir, dass „Personen die landen nicht heilig sind“.
> Folgerung = impl(Gelandet, neg(Heilig)).
Wir wollen somit folgendes Ziel prüfen:
> Ziel = impl(konj(Annahme1, Annahme2), Folgerung).
Der logische Ausdruck „Ziel“ wird zur Prüfung in eine konjunktive Normalform überführt. Die konjunktive Normalform besteht aus Konjunktionen bei denen die einzelnen Glieder der Konjunktionen Disjunktionen sind. In jedem solchen Glied kommt dabei jedes Atom negiert oder nicht negiert vor (link). Zur Transformation definieren wir die Funktionen nnf/1, distrib/2, knf/1. Die Funktion nnf/1 - Negation Normalform - zieht alle Negationen zu den Atomen (De Morgansches Gesetzt). distrib/1 wendet das Distributivgesetz an, knf/1 berechnet die konjunktive Normalform.
% Negationsnormalform (Negation nur auf Ebene der Atome)
nnf({atom, P}) -> atom(P);
nnf({neg, {atom, P}}) -> neg(atom(P));
nnf({neg, {neg, P}}) -> nnf(P);
nnf({neg, {konj, P, Q}}) -> nnf(disj(neg(P), neg(Q)));
nnf({neg, {disj, P, Q}}) -> nnf(konj(neg(P), neg(Q)));
nnf({konj, P, Q}) -> konj(nnf(P), nnf(Q));
nnf({disj, P, Q}) -> disj(nnf(P), nnf(Q)).
% Distributiv Gesetz
distrib(P, {konj, Q, R}) -> konj(distrib(P,Q), distrib(P,R));
distrib({konj, Q, R}, P) -> konj(distrib(Q,P), distrib(R,P));
distrib(P,Q) -> disj(P,Q).
% Konjunktive Normalform
knf({konj, P,Q}) -> konj(knf(P), knf(Q));
knf({disj, P, Q}) -> distrib(knf(P), knf(Q));
knf(P) -> P.
Die eigentliche Tautologie wird geprüft, in dem für jedes Konjunktionsglied geschaut wird, ob dieses zu true ausgewertet werden kann. Um dieses zu machen schauen wir, ob mindestens ein Atom positiv also auch negativ in der Disjunktion vorkommt. Wenn das der Fall ist, wird dieses Konjunktionsglied true. Dieses geschieht mit Hilfe der Funktionen positiv/1 und negativ/1. Diese Funktionen erstellen aus jeder Disjunktion Listen der Vorkommen positiver und negativer Atome. Wir prüfen dann, ob deren Schnittmenge nicht leer ist.
% Alle positiven Atome in den Disjunktion
positiv({atom, P}) -> [P];
positiv({neg, {atom, _}}) -> [];
positiv({disj, P, Q}) -> positiv(P) ++ positiv(Q);
positiv(_) -> throw(keine_normalform).
% Alle negativen Atome in der Disjunktion
negativ({atom, _}) -> [];
negativ({neg, {atom, P}}) -> [P];
negativ({disj, P, Q}) -> negativ(P) ++ negativ(Q);
negativ(_) -> throw(keine_normalform).
ist_tautologie({konj, P,Q}) -> ist_tautologie (P) andalso ist_tautologie (Q);
ist_tautologie(P) ->
Pos = sets:from_list(positiv(P)),
Neg = sets:from_list(negativ(P)),
Result = sets:intersection(Pos, Neg),
sets:size(Result) > 0.
Hier der Funktionsaufruf:
> ist_tautologie(knf(nnf(Ziel))).
true
Damit wird unser Ziel zu true und der Ausdruck ist somit eine Tautologie. Damit sind Personen, die landen, nicht heilig.
Hinweis: Diese Art Tautologie Checker ist in der Praxis kaum praktikabel. Für seriöse Probleme werden andere Codierungen und Lösungsstrategien verwendet, etwa mit Binären Entscheiddiagrammen, kurz BDDs. Siehe auch hier.
Parallele Funktionen Höherer Ordnung - pmap
13 06 2009
Erlangs leichtgewichtiges Prozessmodell ermöglicht die Erzeugung von mehreren tausend Prozessen. Aus diesen Grund lassen sich in Erlang Programme entwickelt, die gut mit der Anzahl der Kerne auf einem Multicore Prozessor skalieren. Eine tolle Sache, wenn wir davon ausgehen können, dass Multicore Prozessoren der Zukunft dutzende oder sogar hunderte Kerne haben, denn so profitieren unsere Erlang Programme ohne weiteres Zutun davon.
Beispiel:
Die Funktion pmap(Fun, Liste) -- in Anlehnung an die sequentielle High Order Funktion map -- wertet alle Elemente der Liste parallel aus. Dazu wird für jedes Element der Liste ein eigener Prozess erzeugt; dieser Prozess führt dann Fun(Element) aus. Hat Fun das Ergebnis erarbeitet, wird dieses dem „Vaterprozess“ in die Mailbox gestellt. Dieser sammelt alle Ergebnisse in der Reihenfolge der Originalliste auf und liefert als Ergebnis eine Liste; das Resultat von pmap.
-module(pm).
-export([pmap/2]).
pmap(Fun, Liste) ->
Pid = self(),
Ref=erlang:make_ref(),
Pids=lists:map(fun(Element) ->
spawn(fun() -> do_fun(Pid, Ref, Fun, Element) end) end,
sammel(Pids, Ref).
do_fun(VaterPid, Ref, Fun, Element) ->
VaterPid ! {self(), Ref, catch(Fun(Element))}.
sammel([Pid | T], Ref) ->
receive
{Pid, Ref, Ergebnis} -> [Ergebnis | sammel(T, Ref)]
end;
sammel([], _) -> [].
Folgendes Beispiel berechnet die Fakultät parallel für alle Elemente einer Liste von 1000 Zufallswerten zwischen 0 und 30.
test_pmap(Max) ->
List = lists:seq(1, Max),
TestData = lists:map(fun(_) -> random:uniform(30) end, List),
pm:pmap(fun fac/1, TestData).
> test_pmap(1000).
Hinweise:
Da mit jedem Listenelement ein neuer Prozess erzeugt wird, sollte man bei grossen Listen ein wenig aufpassen.
Damit Prozesse von Multicore Prozessoren profitieren, sollten sie
(a) keine Seiteneffekte haben,
(b) möglichst rechenintensive Aufgaben durchführen und
(c) Meldungen, die sie versenden, möglichst klein sein .
Beispiel:
Die Funktion pmap(Fun, Liste) -- in Anlehnung an die sequentielle High Order Funktion map -- wertet alle Elemente der Liste parallel aus. Dazu wird für jedes Element der Liste ein eigener Prozess erzeugt; dieser Prozess führt dann Fun(Element) aus. Hat Fun das Ergebnis erarbeitet, wird dieses dem „Vaterprozess“ in die Mailbox gestellt. Dieser sammelt alle Ergebnisse in der Reihenfolge der Originalliste auf und liefert als Ergebnis eine Liste; das Resultat von pmap.
-module(pm).
-export([pmap/2]).
pmap(Fun, Liste) ->
Pid = self(),
Ref=erlang:make_ref(),
Pids=lists:map(fun(Element) ->
spawn(fun() -> do_fun(Pid, Ref, Fun, Element) end) end,
sammel(Pids, Ref).
do_fun(VaterPid, Ref, Fun, Element) ->
VaterPid ! {self(), Ref, catch(Fun(Element))}.
sammel([Pid | T], Ref) ->
receive
{Pid, Ref, Ergebnis} -> [Ergebnis | sammel(T, Ref)]
end;
sammel([], _) -> [].
Folgendes Beispiel berechnet die Fakultät parallel für alle Elemente einer Liste von 1000 Zufallswerten zwischen 0 und 30.
test_pmap(Max) ->
List = lists:seq(1, Max),
TestData = lists:map(fun(_) -> random:uniform(30) end, List),
pm:pmap(fun fac/1, TestData).
> test_pmap(1000).
Hinweise:
Da mit jedem Listenelement ein neuer Prozess erzeugt wird, sollte man bei grossen Listen ein wenig aufpassen.
Damit Prozesse von Multicore Prozessoren profitieren, sollten sie
(a) keine Seiteneffekte haben,
(b) möglichst rechenintensive Aufgaben durchführen und
(c) Meldungen, die sie versenden, möglichst klein sein .
Hot Code Replacement
12 05 2009
Höchstverfügbare Systeme, bei denen eine Downtime keine Option ist, brauchen Verfahren um Korrekturen in das produktive System einzuspielen, ohne das dieses runtergefahren und neugestartet werden muss. Erlang bietet hier die Möglichkeit des Codeaustauches zur Laufzeit.
Beispiel:
-module(m).
-export([loop/0, start/0]).
start() ->
spawn(m, loop, []).
tu_dies() ->
io:format("Falsche Implementierung~n").
tu_das() ->
io:format("jawoll~n").
%
% Process Loop
%
loop() ->
receive
code_switch ->
m:loop();
tu_dies ->
tu_dies(),
loop();
tu_das ->
tu_das(),
loop()
end.
Starten wir zwei Prozesse in der Shell und senden Ihnen einige Meldungen:
> P1 = m:start().
...
> P2 = m:start().
...
> P1 ! tu_dies.
Fehlerhafte Implementierung
> P1 ! tu_das.
ok
> P2 ! tu_dies.
Fehlerhafte Implementierung
Die Funktion tu_dies/0 hat einen Fehler, den wir nun korrigieren wollten. Nehmen wir im Modul m folgende Korrektur vor ...
tu_dies() ->
io:format("Korrekte Implementierung~n").
und compilieren und laden es mit:
> c(m).
Schauen wir was passiert:
> P1 ! tu_dies.
Fehlerhafte Implementierung
> P2 ! tu_dies.
Fehlerhafte Implementierung
Beide Prozess verwenden also noch die alte Implementierung.
Nun Informieren wir P1, dass er das korrigierte Modul nehmen soll:
> P1 ! code_switch.
Und prüfen dies auch gleich:
> P1 ! tu_dies.
Korrekte Implementierung
> P2 ! tu_dies.
Fehlerhafte_Implementierung
Der qualitfizierte Aufruf m:loop() im Rumpf zu code_switch ist per Definition immer an das aktuelle Module m gebunden; in unserem Fall also das korrigierte und neu geladene Modul. Auffällig ist, dass bestehende Prozesse (hier etwa P2) weiter mit der alten Implementierung arbeiten können. Das Erlang-System kann nämlich zwei Versionen eines Moduls, die alte und die aktuelle, halten. Dieses Verfahren ist sinnvoll, da P2 ja gerade wichtige Aufgaben bearbeiten könnte, die erst abgeschlossen werden sollten, bevor die neue Implementierung verwendet werden soll. Hierzu noch ein Hinweis: Wird versucht eine weitere Version zu laden, wird die alte Version aus dem System entfernt und die bisher aktive zur alten Version. Etwaige Prozesse, die das alte Modul verwenden, werden dann jedoch terminiert.
Erlang - The Smarter Way Of Programming
Beispiel:
-module(m).
-export([loop/0, start/0]).
start() ->
spawn(m, loop, []).
tu_dies() ->
io:format("Falsche Implementierung~n").
tu_das() ->
io:format("jawoll~n").
%
% Process Loop
%
loop() ->
receive
code_switch ->
m:loop();
tu_dies ->
tu_dies(),
loop();
tu_das ->
tu_das(),
loop()
end.
Starten wir zwei Prozesse in der Shell und senden Ihnen einige Meldungen:
> P1 = m:start().
...
> P2 = m:start().
...
> P1 ! tu_dies.
Fehlerhafte Implementierung
> P1 ! tu_das.
ok
> P2 ! tu_dies.
Fehlerhafte Implementierung
Die Funktion tu_dies/0 hat einen Fehler, den wir nun korrigieren wollten. Nehmen wir im Modul m folgende Korrektur vor ...
tu_dies() ->
io:format("Korrekte Implementierung~n").
und compilieren und laden es mit:
> c(m).
Schauen wir was passiert:
> P1 ! tu_dies.
Fehlerhafte Implementierung
> P2 ! tu_dies.
Fehlerhafte Implementierung
Beide Prozess verwenden also noch die alte Implementierung.
Nun Informieren wir P1, dass er das korrigierte Modul nehmen soll:
> P1 ! code_switch.
Und prüfen dies auch gleich:
> P1 ! tu_dies.
Korrekte Implementierung
> P2 ! tu_dies.
Fehlerhafte_Implementierung
Der qualitfizierte Aufruf m:loop() im Rumpf zu code_switch ist per Definition immer an das aktuelle Module m gebunden; in unserem Fall also das korrigierte und neu geladene Modul. Auffällig ist, dass bestehende Prozesse (hier etwa P2) weiter mit der alten Implementierung arbeiten können. Das Erlang-System kann nämlich zwei Versionen eines Moduls, die alte und die aktuelle, halten. Dieses Verfahren ist sinnvoll, da P2 ja gerade wichtige Aufgaben bearbeiten könnte, die erst abgeschlossen werden sollten, bevor die neue Implementierung verwendet werden soll. Hierzu noch ein Hinweis: Wird versucht eine weitere Version zu laden, wird die alte Version aus dem System entfernt und die bisher aktive zur alten Version. Etwaige Prozesse, die das alte Modul verwenden, werden dann jedoch terminiert.
Erlang - The Smarter Way Of Programming
Fehlerbehandlung in nebenläufigen und verteilten Prozessen
11 05 2009
Erlang hat einen eingebauten Mechanismus zur Fehlerbehandlung zwischen Prozessen. Ein abgestürtzter Prozess sendet hierbei allen „gelinkten“ Prozessen eine Meldung der Form {'EXIT', SomePid, FehlerGrund}, auch Exit-Meldung genannt. Die Empfängerprozesse können diese Meldung auswerten und entsprechend reagieren (Protokollierung, Prozess neu starten, Alarm auslösen, ...). Mit dieser Eigenschaft können auch einfach hierarchische Strukturen zur Überwachung von Prozessen gebaut werden. Das Erlang/OTP macht mit seinen Supervisor-Trees davon gebrauch. In einem späteren Eintrag mehr dazu.
Beispiel:
Wir haben in diesem Beisipel zwei Prozesse, einen Error_Handler Prozess und einen Worker Prozess. Der Worker wird beim Erzeugen des Error_Handlers durch die Funktion link registriert. Damit der Error_Handler Exit-Meldungen vom Worker empfangen kann, ist er zudem als Systemprozess zu markieren. Dieses geschieht durch den Funktionsaufruf process_flag(trap_exit, true). Andernfalls würde der Error_Handler nach Erhalt der Exit-Meldung ebenfalls terminieren.
Unser Worker kann zwei Messages behandeln: msg_ok und msg_error. Bei der Bearbeitung der Message msg_error wird eine nicht zulässige Division durch Null ausgeführt, was zu einer unbehandelten Exception führt und ein Terminieren des Workers bewirkt. Diese unbehandelte Exception wird dem Error_Handler durch die Exit-Meldung signalisiert (FehlerGrund).
-module(process).
-export([start_error_handler/1, error_handler/1, start/0, proc/0]).
%
% Error_Handler
%
start_error_handler(Pid) ->
spawn(process, error_handler, [Pid]).
error_handler(Pid) ->
process_flag(trap_exit, true),
link(Pid),
receive
{'EXIT', SomePid, FehlerGrund} ->
io:format("~p terminiert, da ~p~n", [SomePid,FehlerGrund])
end.
%
% Prozess Worker
%
start() ->
spawn(process, proc, []).
proc() ->
receive
msg_ok ->
io:format("Alles ok~n"),
proc();
msg_error ->
io:format("Autsch ... Fehler ~n"),
1/0 % HIER FEHLER
end.
In der Shell:
> Worker = process:start().
> process:start_error_handler(Worker).
> Worker ! msg_ok.
Alles ok
> Worker ! msg_error.
Autsch ... Fehler
...
<0.38.0> terminiert, da {badarith,[{process,proc,0}]}
Es können mehrere Error_Handler auf dem Worker Prozess registriert werden. Diese Handler erhalten beim Absturz des Workers alle eine Exit-Meldung.
Dieses Trap-Exit Verfahren funktioniert natürlich auch bei auf mehrere Erlang-Knoten verteilte Prozesse.
Beispiel:
Wir haben in diesem Beisipel zwei Prozesse, einen Error_Handler Prozess und einen Worker Prozess. Der Worker wird beim Erzeugen des Error_Handlers durch die Funktion link registriert. Damit der Error_Handler Exit-Meldungen vom Worker empfangen kann, ist er zudem als Systemprozess zu markieren. Dieses geschieht durch den Funktionsaufruf process_flag(trap_exit, true). Andernfalls würde der Error_Handler nach Erhalt der Exit-Meldung ebenfalls terminieren.
Unser Worker kann zwei Messages behandeln: msg_ok und msg_error. Bei der Bearbeitung der Message msg_error wird eine nicht zulässige Division durch Null ausgeführt, was zu einer unbehandelten Exception führt und ein Terminieren des Workers bewirkt. Diese unbehandelte Exception wird dem Error_Handler durch die Exit-Meldung signalisiert (FehlerGrund).
-module(process).
-export([start_error_handler/1, error_handler/1, start/0, proc/0]).
%
% Error_Handler
%
start_error_handler(Pid) ->
spawn(process, error_handler, [Pid]).
error_handler(Pid) ->
process_flag(trap_exit, true),
link(Pid),
receive
{'EXIT', SomePid, FehlerGrund} ->
io:format("~p terminiert, da ~p~n", [SomePid,FehlerGrund])
end.
%
% Prozess Worker
%
start() ->
spawn(process, proc, []).
proc() ->
receive
msg_ok ->
io:format("Alles ok~n"),
proc();
msg_error ->
io:format("Autsch ... Fehler ~n"),
1/0 % HIER FEHLER
end.
In der Shell:
> Worker = process:start().
> process:start_error_handler(Worker).
> Worker ! msg_ok.
Alles ok
> Worker ! msg_error.
Autsch ... Fehler
...
<0.38.0> terminiert, da {badarith,[{process,proc,0}]}
Es können mehrere Error_Handler auf dem Worker Prozess registriert werden. Diese Handler erhalten beim Absturz des Workers alle eine Exit-Meldung.
Dieses Trap-Exit Verfahren funktioniert natürlich auch bei auf mehrere Erlang-Knoten verteilte Prozesse.
Prozesse verteilen
08 05 2009
Weil es so schön einfach ist, gleich die Verteilung der Prozesse auf unterschiedliche Rechner hinterher: Erlang Prozesse, die in unterschiedlichen Erlang Laufzeitumgebungen (Erlang-Knoten) existieren, können sich gegenseitig ebenso einfach Meldungen zukommen lassen, wie solche in einer Umgebung. Dabei ist es egal, auf welchem physikalischen Rechner die Erlang Knoten effektiv laufen.
Beispiel:
Angenommen der kleine Robi aus dem vorherigen Bespiel steht über das Netzwerk (WLAN) mit einem Kommandierungsrechner in Verbindung, und auf Robi (mit der Rechneradresse: robi.robot.net) sowie auf dem Rechner laufen eigene Erlang Umgebungen (Namen der Erlang Knoten: node).
Wenn auf Robi der Steuerungsprozess gestartet (robot:start()) wurde, können wir in der Erlang-Shell des Kommandierungsrechners durch die schlichte Eingabe von
> {robi,’node@robi.robot.net’} ! {vorwaerts, 10}
Robi einen Befehl zustellen.
Vorausetzung ist lediglich, dass die beiden Knoten mit entsprechenden Parametern (Knotenname, gemeinsames Magic Cookie) gestartet werden.

Magic Cookie, was’n das? Erlang Knoten auf unterschiedlichen Rechnern können sich nur gegenseitig Meldungen zustellen, wenn sie eine gemeinsame Zeichenkette - den Magic Cookie - kennen. Eine solche gemeinsame Zeichenkette muss bei jeder an der Kommunikation teilnehmenden Umgebung hinterlegt werden.
Beispiel:
Angenommen der kleine Robi aus dem vorherigen Bespiel steht über das Netzwerk (WLAN) mit einem Kommandierungsrechner in Verbindung, und auf Robi (mit der Rechneradresse: robi.robot.net) sowie auf dem Rechner laufen eigene Erlang Umgebungen (Namen der Erlang Knoten: node).
Wenn auf Robi der Steuerungsprozess gestartet (robot:start()) wurde, können wir in der Erlang-Shell des Kommandierungsrechners durch die schlichte Eingabe von
> {robi,’node@robi.robot.net’} ! {vorwaerts, 10}
Robi einen Befehl zustellen.
Vorausetzung ist lediglich, dass die beiden Knoten mit entsprechenden Parametern (Knotenname, gemeinsames Magic Cookie) gestartet werden.
Magic Cookie, was’n das? Erlang Knoten auf unterschiedlichen Rechnern können sich nur gegenseitig Meldungen zustellen, wenn sie eine gemeinsame Zeichenkette - den Magic Cookie - kennen. Eine solche gemeinsame Zeichenkette muss bei jeder an der Kommunikation teilnehmenden Umgebung hinterlegt werden.
Selective Receive
08 05 2009
Der Meldungsaustausch zwischen Prozessen basiert auf asynchroner Kommunikation über Mailboxen. Der Vorteil hierbei ist, dass Sender und Empfänger weitgehend autonom voneinander bleiben können. Sendet ein Prozess A eine Meldung an einen Prozess B, kann sich Prozess A direkt nach dem Absenden der Meldung wieder anderen Aufgaben widmen. Ebenso ist es egal, was Prozess B zur Zeit des Absendens macht, denn die Meldung von Prozess A wird zunächst in der Mailbox (Briefkasten) vom Prozess B abgelegt.
Das „Selective Receive“ vom Prozess B prüft mit der Möglichkeit des Musterabgleichs (engl. Pattern Matching), ob eine Meldung aus der Mailbox einem der Meldungsmuster entspricht. Ist das der Fall und ist zusätzlich der entsprechende Bewacher true, wird die Meldung aus der Mailbox genommen und der zugehörige Rumpf-Ausdruck ausgewertet. Befindet sich keine Meldung in der Prozessmailbox wird, wenn definiert, nach einer Zeit TimeOut der TimeOutAusdruck ausgewertet.
Hier das Schema des Receive-Ausdrucks:
receive
MeldungsMuster1 [when Bewacher1] ->
Ausdruck1;
MeldungsMuster2 [when Bewacher2] ->
Ausdruck2;
...
[after
TimeOut ->
TimeOutAusdruck]
end
Beispiel:
Hier ein kleines Beispiel für den „Steuerungsprozess“ für den „Roboter“ Robi:
-module(robot).
-export([start/0, loop/1]).
start() ->
register(robi, spawn(robot, loop, [10000])).
loop(TimeOut) ->
receive
{vorwaerts, AnzahlSchritte} ->
io:format("gehe ~p schritte vorwaerts~n", [AnzahlSchritte]),
loop(TimeOut);
{rueckwaerts, AnzahlSchritte} ->
io:format("gehe ~p schritte rueckwaerts~n", [AnzahlSchritte]),
loop(TimeOut);
{drehe_rechts, Grad} ->
io:format("drehe ~p grad rechts~n", [Grad]),
loop(TimeOut);
{drehe_links, Grad} ->
io:format("drehe ~p grad links~n", [Grad]),
loop(TimeOut);
halt ->
io:format("halte an~n"),
loop(TimeOut)
after TimeOut->
io:format("piep piep piep ~n"),
loop(TimeOut)
end.
In der Shell den Steuerungsprozess starten ...
>robot:start().
... und Befehle senden
>robi ! {vorwaerts, 20}.
gehe 20 schritte vorwaerts
>robi ! {drehe_rechts, 45}.
drehe 45 grad rechts
>robi ! halt.
halte an
Und falls keine Befehle kommen:
piep piep piep
Noch ein paar Hinweise zur Arbeitsweise vom Receive:
Das „Selective Receive“ vom Prozess B prüft mit der Möglichkeit des Musterabgleichs (engl. Pattern Matching), ob eine Meldung aus der Mailbox einem der Meldungsmuster entspricht. Ist das der Fall und ist zusätzlich der entsprechende Bewacher true, wird die Meldung aus der Mailbox genommen und der zugehörige Rumpf-Ausdruck ausgewertet. Befindet sich keine Meldung in der Prozessmailbox wird, wenn definiert, nach einer Zeit TimeOut der TimeOutAusdruck ausgewertet.
Hier das Schema des Receive-Ausdrucks:
receive
MeldungsMuster1 [when Bewacher1] ->
Ausdruck1;
MeldungsMuster2 [when Bewacher2] ->
Ausdruck2;
...
[after
TimeOut ->
TimeOutAusdruck]
end
Beispiel:
Hier ein kleines Beispiel für den „Steuerungsprozess“ für den „Roboter“ Robi:
-module(robot).
-export([start/0, loop/1]).
start() ->
register(robi, spawn(robot, loop, [10000])).
loop(TimeOut) ->
receive
{vorwaerts, AnzahlSchritte} ->
io:format("gehe ~p schritte vorwaerts~n", [AnzahlSchritte]),
loop(TimeOut);
{rueckwaerts, AnzahlSchritte} ->
io:format("gehe ~p schritte rueckwaerts~n", [AnzahlSchritte]),
loop(TimeOut);
{drehe_rechts, Grad} ->
io:format("drehe ~p grad rechts~n", [Grad]),
loop(TimeOut);
{drehe_links, Grad} ->
io:format("drehe ~p grad links~n", [Grad]),
loop(TimeOut);
halt ->
io:format("halte an~n"),
loop(TimeOut)
after TimeOut->
io:format("piep piep piep ~n"),
loop(TimeOut)
end.
In der Shell den Steuerungsprozess starten ...
>robot:start().
... und Befehle senden
>robi ! {vorwaerts, 20}.
gehe 20 schritte vorwaerts
>robi ! {drehe_rechts, 45}.
drehe 45 grad rechts
>robi ! halt.
halte an
Und falls keine Befehle kommen:
piep piep piep
Noch ein paar Hinweise zur Arbeitsweise vom Receive:
- Meldungen in der Mailbox werden in der Reihe des Eintreffens abgearbeitet.
- Passt keine Meldung auf ein entsprechendes Muster im receive-Ausdruck, wird der Prozess suspendiert und erst durch eine neueintreffende Meldung, bzw. einen TimeOut, reaktiviert.
- Meldungen für die der Musterabgleich nicht erfolgreich war, werden in eine „Sichere Warteschlange“ abgelegt, und zwar solange, bis eine neue Meldung in der Mailbox landet oder der Timer abläuft. In diesem Falls werden die Meldungen aus der „Sicheren Warteschlange“ wieder in die Mailxbox übernommen.
Nochmal List Comprehensions
26 04 2009
Weil es so schön ist, hier noch zwei weitere Beispiele von List Comprehensions.
Beispiel 1: Die Funktion pyth(N) erzeugt eine Liste aller Integer-Tripel {A, B, C} für die A^2 + B^2 = C^2 und die Summe der Seiten kleiner oder gleich N gilt.
pyth(N) ->
[ {A,B,C} ||
A <- lists:seq(1,N),
B <- lists:seq(1,N),
C <- lists:seq(1,N),
A+B+C =< N,
A*A+B*B == C*C
].
> beispiele:pyth(32).
[{3,4,5},{4,3,5},{5,12,13},{6,8,10},{8,6,10},{12,5,13}]
Beispiel 2: Die Funktion sort(N) sortiert eine Liste N.
sort([Pivot| T]) ->
sort([X || X <- T, X < Pivot]) ++ [Pivot] ++
sort([X || X <- T, X >= Pivot]);
sort([]) -> [].
Der Ausdruck [ X || X <- T, X < Pivot] sammelt alle Elemente X auf, die aus der Liste T stammen und dem Ausdruck X < Pivot genügen.
++ ist die Infix-Darstellung der Erlang Append-Funktion, mit der Listen konkateniert werden. Das Muster [Pivot | T ] im Funktionskopf zerlegt die Liste in zwei Teile: Pivot ist der Listenkopf, T der Rest der Liste.
> beispiele:sort([4,3,14,5,4,3]).
[3,3,4,4,5,14]
Beispiel 1: Die Funktion pyth(N) erzeugt eine Liste aller Integer-Tripel {A, B, C} für die A^2 + B^2 = C^2 und die Summe der Seiten kleiner oder gleich N gilt.
pyth(N) ->
[ {A,B,C} ||
A <- lists:seq(1,N),
B <- lists:seq(1,N),
C <- lists:seq(1,N),
A+B+C =< N,
A*A+B*B == C*C
].
> beispiele:pyth(32).
[{3,4,5},{4,3,5},{5,12,13},{6,8,10},{8,6,10},{12,5,13}]
Beispiel 2: Die Funktion sort(N) sortiert eine Liste N.
sort([Pivot| T]) ->
sort([X || X <- T, X < Pivot]) ++ [Pivot] ++
sort([X || X <- T, X >= Pivot]);
sort([]) -> [].
Der Ausdruck [ X || X <- T, X < Pivot] sammelt alle Elemente X auf, die aus der Liste T stammen und dem Ausdruck X < Pivot genügen.
++ ist die Infix-Darstellung der Erlang Append-Funktion, mit der Listen konkateniert werden. Das Muster [Pivot | T ] im Funktionskopf zerlegt die Liste in zwei Teile: Pivot ist der Listenkopf, T der Rest der Liste.
> beispiele:sort([4,3,14,5,4,3]).
[3,3,4,4,5,14]
List Comprehensions
13 04 2009
List Comprehensions sind ebenfalls eine elegante Möglichkeit Listenerzeugung sehr kompakt in Erlang auszudrücken. List Comprehensions folgen dem Schema zur Konstruktion mathematischer Mengen.
Beispiel 1:
> S1 = [ 2 * X || X <- [1,2,3,4]]
[2,4,6,8]
S1 ist die Liste aller 2*X wobei X aus der Liste [1,2,3,4] stammt.
List Comprehensions können auch alternativ zu den schon vorgestellten Funktionen auf Listen, wie map und filter, verwendet werden.
Beispiel 2:
> S2 = [Quadrat(X) || X <- [1,2,3,4,5,6,7,8,9], Gerade(X)].
[4,16,36,64]
S2 ist die Liste aller X zum Quadrat (Quadrat(X)), wobei X aus der Liste [1,2,3,4,5,6,7,8,9] stammt und X gerade ist (Gerade(X)).
Quadrat und Gerade sind wie im Blogeintrag „Funktionen auf Listen“ definiert.
In Erlang sind List Comprehensions wie folgt aufgebaut:
[Expr || Qualifier1, ..., QualifierN]
Ein Qualifier ist dabei entweder ein Generator der Form Pattern <- ListExpr oder ein boolescher Test-Ausdruck, genannt filter.
Beispiel 3:
Hier werden die Komponenten M und N aller Paare {M,N} aus der PaarListe addiert, falls M < N ist.
> AddiereGeordPaare =
fun(PaarListe) -> [ M+N || {M,N} <- PaarListe, M < N] end.
...
> AddiereGeordPaare([{2,3}, {2,1}, {7,8}]).
[5,15]
Beispiel 4:
Aus einer Datenbank sollen weibliche Personen, die nach 1970 geboren wurden, selektiert werden. Der Einfachheit halber ist unsere Datenbank eine Variable DB an die eine Liste von Personen-Strukturen gebunden ist.
> DB =
[{person,"Hans",maennlich,1971},
{person,"Franz",maenlich,1990},
{person,"Michaela",weiblich,1988},
{person,"Steffi",weiblich,1978},
{person,"Boris",maennlich,1969},
{person,"Petra",weiblich,1968}].
Die entsprechende Selektion ist so definiert:
> [ P || P = {person, _, Geschlecht, Geburtsjahr} <- DB,
Geschlecht == weiblich, Geburtsjahr > 1970].
[{person,"Michaela",weiblich,1988},
{person,"Steffi",weiblich,1978}]
Erlang - The Smarter Way of Programming.
Und tschüss.
Beispiel 1:
> S1 = [ 2 * X || X <- [1,2,3,4]]
[2,4,6,8]
S1 ist die Liste aller 2*X wobei X aus der Liste [1,2,3,4] stammt.
List Comprehensions können auch alternativ zu den schon vorgestellten Funktionen auf Listen, wie map und filter, verwendet werden.
Beispiel 2:
> S2 = [Quadrat(X) || X <- [1,2,3,4,5,6,7,8,9], Gerade(X)].
[4,16,36,64]
S2 ist die Liste aller X zum Quadrat (Quadrat(X)), wobei X aus der Liste [1,2,3,4,5,6,7,8,9] stammt und X gerade ist (Gerade(X)).
Quadrat und Gerade sind wie im Blogeintrag „Funktionen auf Listen“ definiert.
In Erlang sind List Comprehensions wie folgt aufgebaut:
[Expr || Qualifier1, ..., QualifierN]
Ein Qualifier ist dabei entweder ein Generator der Form Pattern <- ListExpr oder ein boolescher Test-Ausdruck, genannt filter.
Beispiel 3:
Hier werden die Komponenten M und N aller Paare {M,N} aus der PaarListe addiert, falls M < N ist.
> AddiereGeordPaare =
fun(PaarListe) -> [ M+N || {M,N} <- PaarListe, M < N] end.
...
> AddiereGeordPaare([{2,3}, {2,1}, {7,8}]).
[5,15]
Beispiel 4:
Aus einer Datenbank sollen weibliche Personen, die nach 1970 geboren wurden, selektiert werden. Der Einfachheit halber ist unsere Datenbank eine Variable DB an die eine Liste von Personen-Strukturen gebunden ist.
> DB =
[{person,"Hans",maennlich,1971},
{person,"Franz",maenlich,1990},
{person,"Michaela",weiblich,1988},
{person,"Steffi",weiblich,1978},
{person,"Boris",maennlich,1969},
{person,"Petra",weiblich,1968}].
Die entsprechende Selektion ist so definiert:
> [ P || P = {person, _, Geschlecht, Geburtsjahr} <- DB,
Geschlecht == weiblich, Geburtsjahr > 1970].
[{person,"Michaela",weiblich,1988},
{person,"Steffi",weiblich,1978}]
Erlang - The Smarter Way of Programming.
Und tschüss.
Server mit mehreren Arbeiterprozessen
12 04 2009
Heute zeigen wir ein kleines Beispiel zur Programmierung mit Prozessen. In diesem Beispiel nimmt ein Serverprozess Sortieraufträge eines (oder mehrerer) Klienten entgegen und leitet diese an einen frisch erzeugten Arbeiterprozess (dem Sortierer) weiter. Der Arbeiterprozess verarbeitet den Auftrag und sendet das Ergebnis an seinen Klienten zurück und beendet sich.
Hier der Codeschnippsel:
-module(server).
-export([start/0, loop/0]).
%% erzeuge Serverprozess
start() ->
register(server, spawn(server, loop, [])).
loop() ->
receive
{sortiere, Pid, Liste} ->
erzeuge_sortierer(Pid, Liste),
loop();
_ ->
loop()
end.
%%% erzeuge Sortiererprozess
erzeuge_sortierer(Pid, Liste) ->
spawn(fun() -> sortierer(Pid, Liste) end).
sortierer(Pid, Liste) ->
Ergebnis = lists:sort(Liste),
Pid ! Ergebnis.
Mit server:start() wird der Hauptprozess des Servers gestartet. Die Funktion spawn/1 erzeugt hier den Prozess, wobei die Funktion loop/0 zur Ausführung übergeben wird. Mit register/1 wird der Prozess Identifier - das Ergbnis von spawn/1 - unter den Namen server in der Erlang VM registriert. In der loop/0 verarbeitet der Server eingehende Aufträge der Form {sortierte, Pid, Liste}, wobei Pid der Prozess Identifier des Klienten und Liste die zu sortierende Liste ist. Empfängt (receive ... end) der Server eine solche Nachricht wird mit erzeuge_sortierer/2 ein neuer Prozess erzeugt, dessen Funktion sortierer/2 die eigentliche Sortierung der Liste durchführt und mit Pid ! Ergebnis das Resultat zurück an den Klienten sendet. Der Serverprozess läuft in einer „Endlosschleife“ (beachte den rekursiven Aufruf loop()).
Zum Test hier noch ein einfacher Klient:
-module(client).
-export([sortiere/1]).
sortiere(Liste) ->
server ! {sortiere, self(), Liste},
receive
SortierteListe ->
SortierteListe
end.
Der Klient sendet den Auftrag an den Server und wartet auf die Antwort.
Das Ganze in der Erlang-Shell:
Zunächst den Server starten:
> server:start()
Dann der Sortierauftrag senden.
> client:sortiere([33,21,42,13]).
[13,21,33,42]
Kurz und knapp.
Hier der Codeschnippsel:
-module(server).
-export([start/0, loop/0]).
%% erzeuge Serverprozess
start() ->
register(server, spawn(server, loop, [])).
loop() ->
receive
{sortiere, Pid, Liste} ->
erzeuge_sortierer(Pid, Liste),
loop();
_ ->
loop()
end.
%%% erzeuge Sortiererprozess
erzeuge_sortierer(Pid, Liste) ->
spawn(fun() -> sortierer(Pid, Liste) end).
sortierer(Pid, Liste) ->
Ergebnis = lists:sort(Liste),
Pid ! Ergebnis.
Mit server:start() wird der Hauptprozess des Servers gestartet. Die Funktion spawn/1 erzeugt hier den Prozess, wobei die Funktion loop/0 zur Ausführung übergeben wird. Mit register/1 wird der Prozess Identifier - das Ergbnis von spawn/1 - unter den Namen server in der Erlang VM registriert. In der loop/0 verarbeitet der Server eingehende Aufträge der Form {sortierte, Pid, Liste}, wobei Pid der Prozess Identifier des Klienten und Liste die zu sortierende Liste ist. Empfängt (receive ... end) der Server eine solche Nachricht wird mit erzeuge_sortierer/2 ein neuer Prozess erzeugt, dessen Funktion sortierer/2 die eigentliche Sortierung der Liste durchführt und mit Pid ! Ergebnis das Resultat zurück an den Klienten sendet. Der Serverprozess läuft in einer „Endlosschleife“ (beachte den rekursiven Aufruf loop()).
Zum Test hier noch ein einfacher Klient:
-module(client).
-export([sortiere/1]).
sortiere(Liste) ->
server ! {sortiere, self(), Liste},
receive
SortierteListe ->
SortierteListe
end.
Der Klient sendet den Auftrag an den Server und wartet auf die Antwort.
Das Ganze in der Erlang-Shell:
Zunächst den Server starten:
> server:start()
Dann der Sortierauftrag senden.
> client:sortiere([33,21,42,13]).
[13,21,33,42]
Kurz und knapp.
Funktionen auf Listen
10 04 2009
Funktionen Höherer Ordnung in Kombination mit Listen offenbaren die wahre Eleganz funktionaler Programmierung. Schnell drei einfache Beispiele in Erlang.
Beispiel 1: Die Funktion lists:map(Fun, List1) -> List2 erwartet als Argument eine Funktion Fun sowie eine Liste List1. Auf jedes Element von List1 wird Fun angewendet und das Ergebnis in List2 aufgenommen.
> Quadrat = fun(X) -> X*X end.
...
> lists:map(Quadrat, [1,2,3,4]).
[1,4,9,16]
Wie könnte die Definition der Funktion map aussehen?
map(_,[]) -> [];
map(Fun,[Kopf|Rest]) -> [Fun(Kopf)|map(Fun, Rest)].
Beispiel 2: Die Funktion lists:filter(Praed, List1) -> List2 wendet auf jedes Element die boolesche Funktion Praed an. List2 ist eine Liste der Elemente, für die Praed true ist.
> Gerade = fun(X) -> (X rem 2) =:= 0 end.
...
> lists:filter(Gerade, [1,2,3,4]).
[2,4]
Wie könnte filter definiert sein?
filter(Praed, [Kopf|Rest]) ->
case Praed(Kopf) of
true -> [Kopf|filter(Praed, Rest)] ;
false -> filter(Praed, Rest)
end.
Beispiel 3: Die Funktion lists:foldl(Fun, Acc0, List) -> Acc1 faltet rekursiv die Liste List gemäss der Funktion Fun und einem Startwert Acc0.
> Add = fun(X,Y) -> X+Y end.
...
> lists:foldl(Add,0,[1,2,3,4,5]).
15
Der Aufruf foldl(Add,0,[1,2,3,4,5]) entspricht somit Add(5, Add(4, Add(3, Add(2, Add(1, 0)))))
Die Funktion foldl ist wie folgt definiert:
foldl(Fun, Acc, []) -> Acc;
foldl(Fun, Acc, [Kopf|Rest]) ->
foldl(Fun, Fun(Kopf, Acc), Rest).
Das macht spass.
Beispiel 1: Die Funktion lists:map(Fun, List1) -> List2 erwartet als Argument eine Funktion Fun sowie eine Liste List1. Auf jedes Element von List1 wird Fun angewendet und das Ergebnis in List2 aufgenommen.
> Quadrat = fun(X) -> X*X end.
...
> lists:map(Quadrat, [1,2,3,4]).
[1,4,9,16]
Wie könnte die Definition der Funktion map aussehen?
map(_,[]) -> [];
map(Fun,[Kopf|Rest]) -> [Fun(Kopf)|map(Fun, Rest)].
Beispiel 2: Die Funktion lists:filter(Praed, List1) -> List2 wendet auf jedes Element die boolesche Funktion Praed an. List2 ist eine Liste der Elemente, für die Praed true ist.
> Gerade = fun(X) -> (X rem 2) =:= 0 end.
...
> lists:filter(Gerade, [1,2,3,4]).
[2,4]
Wie könnte filter definiert sein?
filter(Praed, [Kopf|Rest]) ->
case Praed(Kopf) of
true -> [Kopf|filter(Praed, Rest)] ;
false -> filter(Praed, Rest)
end.
Beispiel 3: Die Funktion lists:foldl(Fun, Acc0, List) -> Acc1 faltet rekursiv die Liste List gemäss der Funktion Fun und einem Startwert Acc0.
> Add = fun(X,Y) -> X+Y end.
...
> lists:foldl(Add,0,[1,2,3,4,5]).
15
Der Aufruf foldl(Add,0,[1,2,3,4,5]) entspricht somit Add(5, Add(4, Add(3, Add(2, Add(1, 0)))))
Die Funktion foldl ist wie folgt definiert:
foldl(Fun, Acc, []) -> Acc;
foldl(Fun, Acc, [Kopf|Rest]) ->
foldl(Fun, Fun(Kopf, Acc), Rest).
Das macht spass.
Fakultät
05 04 2009
Was das Hello World für imperative Programmierung ist, ist die Fakultätsfunktion für die Welt der funktionalen Programmierung. Hier der Klassiker in Erlang:
-module(beispiele).
-export([fac/1]).
fac(N) when N > 0 ->
N * fac(N-1);
fac(0) ->
1.
Die Funktion fac ist innerhalb des Moduls beispiele definert. Mit der Export Anweisung wird auf die Funktion fac von ausserhalb des Moduls zugreifbar. Fac/1 bedeutet, dass die Funktion nur ein Argument erwartet. Der When-Ausdruck der Funktion fac, ein boolesche Ausdruck, wird Guard genannt. Falls der Guard zu true evaluiert, wird der Rumpf der Funktion (alles nach dem ->) ausgewertet. Im Rumpf folgt der rekursive Aufruf fac(N-1).
Die Auswertung der Funktion kann direkt an der Erlang-Shell durchgeführt werden. Wie am Ergebnis schnell erkennbar ist, können Erlangs Integerwerte beliebig gross sein. Sie sind nur durch den Hauptspeicher begrenzt.
1>beispiele:fac(40).
815915283247897734345611269596115894272000000000
Einfach und immer wieder schön.
-module(beispiele).
-export([fac/1]).
fac(N) when N > 0 ->
N * fac(N-1);
fac(0) ->
1.
Die Funktion fac ist innerhalb des Moduls beispiele definert. Mit der Export Anweisung wird auf die Funktion fac von ausserhalb des Moduls zugreifbar. Fac/1 bedeutet, dass die Funktion nur ein Argument erwartet. Der When-Ausdruck der Funktion fac, ein boolesche Ausdruck, wird Guard genannt. Falls der Guard zu true evaluiert, wird der Rumpf der Funktion (alles nach dem ->) ausgewertet. Im Rumpf folgt der rekursive Aufruf fac(N-1).
Die Auswertung der Funktion kann direkt an der Erlang-Shell durchgeführt werden. Wie am Ergebnis schnell erkennbar ist, können Erlangs Integerwerte beliebig gross sein. Sie sind nur durch den Hauptspeicher begrenzt.
1>beispiele:fac(40).
815915283247897734345611269596115894272000000000
Einfach und immer wieder schön.
