Lab01
- Shell cheat sheet
- Actor model
- Primo programma
- Numeri
- Atomi
- Tuple
- Liste
- Stringhe
- Control Sequences
- Assegnamento
- Funzioni
- Funzioni anonime (lambda)
- Function References
- Guards
- Moduli
- Macros
- Map, Filter, Reduce
- List Comprehension
- Concorrenza: introduzione
- Invio di messaggi
- Ricezione di messaggi
- Registrare un processo
- BIFs (Built-In Functions)
- Gestione degli errori
Questi comandi funzionano solo nella shell, non possono essere utilizzati nei file sorgente.
help().-> mostra lista comandiq().-> shutdown controllato (tutti i file aperti vengono flushati e chiusi, i database aperti vengono fermati, ecc.)halt()-> shutdown immediatof()-> unbound di tutte le variabilif(X)-> unbound della variabile Xc(Mod)-> compila e carica il modulo Modpwd()-> stampa il path della cartella correntels()-> elenca i nomi dei file nella cartella correntecd(Dir)-> cambia la cartella corrente in Dir
Ogni oggetto è un attore (actor), con un comportamento definito e una mailbox. Gli attori comunicano tra di loro tramite le mailbox (quindi si scambiano messaggi). Ogni attore è implementato come un thread leggero a livello utente, pertanto è caratterizzato da un indirizzo univoco (pid).
Quando un attore riceve un messaggio:
- può inviare altri messaggi a sua volta
- può creare altri attori
- può eseguire un azione
- può trattare i messaggi successivi in modo differente
In generale:
- Lo scambio di messaggi avviene in modo asincrono
- il mittente non aspetta che il messaggio venga ricevuto dopo averlo inviato
- non ci sono garanzie sull'ordine di ricevimento dei messaggi
- Memoria non condivisa tra diversi attori
- le informazioni sullo stato sono richieste e inviate solo tramite messaggi
- la manipolazione dello stato avviene tramite scambio di messaggi
% Nel file factorial.erl
-module(factorial).
-export([factorial/1]).
factorial(0) -> 1;
factorial(N) -> N * factorial(N-1).% Nella shell di erlang
> c(factorial).
{ok,factorial}
> factorial:factorial(7).
5040
> factorial:factorial(100).
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000(le parentesi angolari servono solo per identificare una label, non vanno inserite!
<base> → base)
- E' possibile specificare la base del numero con la seguente sintassi:
<base>#<num> - E' possibile usare la notazione scientifica
- Si può ottenere il char code di un carattere con
$<char>
> 10.
10
> -22.
-22
> 16#FF.
255
> 2#101.
5
> 17#G.
16
> $A.
65
> $G.
71
> -12.35e-2.
-0.1235
> 0.7.
0.7Gli atomi devono iniziare con una lettera minuscola e possono contenere l'underscore _ o la chiocciola @. Se racchiusi tra apici possono iniziare con la lettera maiuscola e contenere caratteri speciali.
Gli atomi sono globali e vengono usati per rappresentare valori costanti.
Il valore di un atomo è l'atomo stesso.
> test_@atomo.
test_@atomo
> 'test & atomo'.
'test & test'
> 'Atomo'.
'Atomo'Possiamo creare una tupla racchiudendo tra graffe alcuni valori, separandoli con la virgola: {val1, val2, val3}
I campi delle tuple non hanno un nome. E' una buona idea mettere un atomo come primo elemento della tupla, in modo da descriverne il contenuto.
{255, 10, 20}
{rgb, 255, 10, 20} % più chiaroLe tuple in Erlang sono eterogenee.
> {"a", "b"}.
{"a","b"}
> {}.
{}
> {colore, {120, 255, 80}}.
{colore,{120,255,80}}
> {{1,2},3}=={1,{2,3}}.
falsePossiamo creare tuple nidificate:
> {color, {red, 255}, {green, 10}, {red, 50}}. % usiamo gli atomi per identificare sia il contenuto generale, sia il contenuto dei vari "campi"
{color,{red,255},{green,10},{red,50}}Per estrarre i valori dalle tuple usiamo il pattern matching:
> {A, B} = {10, 20}.
{10,20}
> A.
10
> B.
20
> C = {color, 255, 10, 20}.
{color,255,10,20}
> {color, R, G, B} = C.
{color,255,10,20}
> R.
255
> G.
10
> B.
20Alcune BIFs utili per lavorare con le tuple: element/2, setelement/3, erlang:delete_element/2, list_to_tuple/1
Possiamo creare una lista racchiudendo tra quadre alcuni valori, separandoli con la virgola: [val1, val2, val3].
> []. % lista vuota
[]
> [1, 2, 3].
[1,2,3]
> [{rgb, 255, 10, 20}, {rgb, 100, 50, 30}].
[{rgb,255,10,20},{rgb,100,50,30}]Come le tuple, anche le liste sono eterogenee:
> [1, "due", 3.0, atomo].
[1,"due",3.0,atomo]Se TL è una lista, allora [HD | TL] è una lista con testa HD e coda TL. La pipe | separa quindi la testa dalla coda.
E' possibile aggiungere più di un elemento all'inizio di TL con la seguente sintassi: [E1, ..., En | TL].
> [1 | []].
[1]
> [1 | [2]].
[1,2]
> [1 | [2 | [3]]].
[1,2,3]
> [1, 2, 3 | [4, 5, 6]].
[1,2,3,4,5,6]Per estrarre i valori dalle liste usiamo il pattern matching. Siano H e T delle variabii unbounded. Se L è una lista non vuota, allora [H | T] = L assegna ad H la testa della lista e a T la coda.
> L = [1, 2, 3, 4].
[1,2,3,4]
> [H | T] = L.
[1,2,3,4]
> H.
1
> T.
[2,3,4]> [First, Second, Third | Rest] = [1,2,3,4,5,6,7].
[1,2,3,4,5,6,7]
> First.
1
> Second.
2
> Third.
3
> Rest.
[4,5,6,7]
> [1, 2, 3, 1, 2, 3] -- [2, 2, 1, 3]. % sottrazione tra liste
[1,3]
> [9, 8, 7] ++ [1, 2, 3]. % concatenazione tra liste
[9,8,7,1,2,3]Erlang non mette a disposizione un tipo stringa, le tratta invece come liste di codici carattere (quindi in genere le operazioni eseguibili sulle liste si possono effettuare anche sulle stringhe).
> [$T, $e, $s, $t].
"Test"Ovviamente sarebbe scomodo utilizzare questa notazione, quindi abbiamo comunque la possibilità di usare i letterali stringa che verranno convertiti automaticamente in liste di codici caratteri.
> "Test".
"Test"
> "Test" == [$T, $e, $s, $t].
true
> [H | T] = "Test".
"Test"
> H. % contiene il codice del carattere 'T'
84
> T.
"est"> [75, 101, 118, 105, 110]. % sono tutti codici di caratteri stampabili
"Kevin"
> [1, 75, 101, 118, 105, 110]. % 1 non è il codice di un carattere stampabile
[1,75,101,118,105,110]
> io:format("~p~n",["abc"]). % stampa come stringa
"abc"
ok
> io:format("~w~n",["abc"]). % stampa come lista di interi
[97,98,99]
okConcatenazione di stringhe
> "Uno "++"due".
"Uno due"
> [$T, $e, $s, $t]++" "++[$1, $2, $3].
"Test 123"Sottrazione tra stringhe
> "AAaabbcc"--"acbA".
"Aabc"
> "AAaabbcc"--"acbAc".
"Aab"Lista completa nella documentazione I più usati sono:
~nnew line~ppretty print~sstampa una stringa senza virgolette~wstampa il termine con la sintassi standard
> io:format("~p~n", ["ciao"]).
"ciao"
ok
> io:format("~s~n", ["ciao"]).
ciao
ok
> io:format("~w~n", ["ciao"]).
[99,105,97,111]
okL'operazione di assegnamento binda un nome ad un valore. Una volta assegnato, non si può modificare.
Le "variabili" devono iniziare con una lettera maiuscola.
_ è anonima.
> A = 1.
1
> B = 2.
2
> A = 4.
** exception error: no match of right hand side value 4
> _ = 4.
4
> _ = 9.
9I binding sono creati mediante pattern matching.
> [Hd|Tl] = [1,2,3,4,5].
[1,2,3,4,5]
> Hd.
1
> Tl.
[2,3,4,5]
> {X, _, Y} = {10, 20, 30}.
{10,20,30}
> X.
10
> Y.
30Vengono analizzate sequenzialmente fino a che una non effettua il match.
name(pattern11 , pattern12 , ..., pattern1n) [when guard1 ] -> body1;
name(pattern21 , pattern22 , ..., pattern2n) [when guard2 ] -> body2;
...
name(patternk1 , patternk2 , ..., patternkn) [when guardk ] -> bodyk.Esempio:
erlang-notes/code/function_example.erl
Lines 1 to 8 in 62cfdba
> (fun(X) -> X*2 end)(10).
20
> Next = fun(K) -> K+1 end.
#Fun<erl_eval.42.3316493>
> Next(3).
4
> Next(5).
6
> Invert = fun(true) -> false; (false) -> true end.
#Fun<erl_eval.42.3316493>
> Invert(true).
false
> Invert(false).
trueEsempi:
Vengono usati per riferire una funzione definita nel modulo corrente o in modulo esterno.
fun NomeFunzioneLocale/Aritàfun Modulo:FunzioneEsterna/Arità
-module(double).
-export([double_list/1]).
double_list(L) -> lists:map(fun double/1, L).
double(X) -> X*2.Una guard sequence è una sequenza di guards, separate da un punto e virgola (;). Una guard sequence è vera (true) se almeno una delle guard di cui è composta è vera.
Inoltre, eventuali guard successive a quella valutata true, non vengono valutate.
Guard1; Guard2; ...; GuardK % guard sequenceUna guard è una sequenza di guard expressions, separate da una virgola (,). Una guard è true se tutte le guard expressions di cui è composta sono vere.
GuardExp1, ..., GuardExpN % guardUna guard expression è un espressione appartenenente ad uno specifico sottoinsieme di tutte le espressioni valide in Erlang. L'elenco di tali espressioni è disponibile qui.
Esempi:
I moduli contengono funzioni, le quali possono essere eseguite sequenzialmente o in parallelo.
I moduli sono l'unità base di Erlang. Sono contenuti in file .erl e vengono compilati in .beam.
La prima riga di un file è un module declaration, e il nome del modulo nella declaration deve essere lo stesso del file senza estensione. Quindi ad esempio nel file mio_modulo.erl possiamo avere:
-module(mio_modulo).
-export([mia_funzione/3]).Questo significa che il modulo mio_modulo esporta una funzione chiamata mia_funzione la quale accetta 3 parametri. Esportare una funzione significa renderla disponibile ad altri moduli. Le funziono non esportate non possono essere chiamate da altri moduli.
Erlang predefinisce le seguenti macro:
?FILEnome del file?MODULEnome del modulo?LINEnumero di linea
Si possono definire nuove macro:
-define(macro_name(arg1, .., argn), body)Esempi:
% utilizzando le funzioni dal modulo lists
> L = lists:seq(1, 20).
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
> lists:map(fun(X) -> X rem 3 == 0 end, L). % map
[false,false,true,false,false,true,false,false,true,false,
false,true,false,false,true,false,false,true,false,false]
> lists:filter(fun(X) -> X rem 3 == 0 end, L). % filter
[3,6,9,12,15,18]
> lists:foldl(fun(Acc, H) -> Acc+H end, 0, L). % reduce
210Esempi:
- implementazione map, filter, reduce
- somma degli elementi in una lista
- somma degli elementi in una lista usando foldl
Forniscono una notazione succinta per la generazione di elementi in una lista. Sintassi:
[Expr || Qualifier1,...,QualifierN]
Exprè un'espressione qualsiasiQualifierpuò essere un generatore o un filtro
Un generatore si scrive come Pattern <- ListExpr, dove ListExpr deve essere un'espressione che viene valutata come lista.
Un filtro può essere un' espressione che deve valere true o false, oppure deve essere una guard expression.
Le variabili nei pattern dei generatori (Pattern) oscurano le variabili precedentemente assegnate.
Una list comprehension restituisce quindi una lista dove gli elementi sono il risultato della valutazione di Expr per ogni combinazione degli elementi dei generatori per i quali tutti i filtri sono true.
> L1=[1,2].
[1,2]
> L2=["a","b"].
["a","b"]
> [{X,Y} || X<-L1, Y<-L2]. % per ogni combinazione ~= prodotto cartesiano
[{1,"a"},{1,"b"},{2,"a"},{2,"b"}]
> [X || X<-L1, Y<-L2].
[1,1,2,2]
> [X || X<-L1, X<-L2]. % "prevale" la X più a destra
["a","b","a","b"]
> [X*2 || X <- [1,2,3,4,5,6,7], X>3]. % raddoppiamo tutti gli elementi maggiori di 3
[8,10,12,14]
> [[A, B] || A<-[a,b], B<-[a,b]].
[[a,a],[a,b],[b,a],[b,b]]
> [X || {r, X} <- [{r, 10}, {r, 20}, {g, 100}, "test"]]. % pattern matching sul generatore
[10,20]Esempi:
case e if sono espressioni, quindi devono restituire un valore. Se non lo fanno, un eccezione viene lanciata a runtime. Per evitarlo si può definire un match all pattern per il case e una true guard per l'if.
case Expr of
Pattern_1 [when Guard_1] -> body_1;
...
Pattern_n [when Guard_n] -> body_n [;]
[other -> else_body]
endif
Guard_1 -> body_1;
...
Guard_n -> body_n [;]
[true -> else_body]
endEsempi:
Erlang mette a disposizione tre funzionalità di base per realizzare la concorrenza:
spawnla funzione built-in (BIF - built-in function) per creare nuovi actors!l'operatore per inviare un messaggio ad un actorreceiveun meccanismo per eseguire il pattern matching sui messaggi nella mailbox
-module(concurrency_test).
-export([start/2, loop/2]).
% spawn(module_name, function, param_list)
start(N, Id) -> spawn(concurrency_test, loop, [N, Id]).
% self() restituisce il pid del thread corrente
loop(0, Id) -> io:format("(pid: ~p - id: ~p) end~n", [self(), Id]);
loop(N, Id) -> io:format("(pid: ~p - id: ~p) ~p~n", [self(), Id, N]), loop(N-1, Id).concurrency_test:loop(10, a), concurrency_test:loop(10, b).concurrency_test:start(10, a), concurrency_test:start(10, b).
La differenza è che nel primo modo eseguiamo due conti alla rovescia in modo sequenziale, nello stesso thread/actor. Nel secondo modo invece creiamo un actor per ogni conto alla rovescia, quindi i conteggi saranno eseguiti in maniera concorrente.
Per poter inviare un messaggio tramite la primitiva !, un actor deve conoscere il pid del destinatario. Nel caso in cui sia necessario ricevere una risposta, il mittente deve includere nel messaggio anche il suo pid. La sintassi è la seguente:
Exp1 ! Exp2
dove Exp1 deve identificare l'attore destinatario e Exp2 può essere qualsiasi espressione valida. Il risultato dell'espressione send (!) è il valore di Exp2.
L'invio non fallisce mai, nemmeno quando il pid specificato non appartiene ad alcun actor. Inoltre l'operazione di send non è bloccante per il mittente.
La ricezione dei messaggi avviene mediante pattern matching:
receive
Pattern_1 [when GuardSeq_1] -> Body_1 ;
...
Pattern_n [when GuardSeq_n] -> Body_n
[after Expr_t -> Body_t]
end.L'actor tenta di prendere dalla mailbox il messaggio più vecchio che effettua il match con uno dei pattern. Se nessun messaggio effettua il match, l'actor aspetta indefinitamente in attesa che arrivi un messaggio valido. Se la clausola after è specificata, l'actor aspetta per un tempo massimo di Expr_t millisecondi, per poi valutare il contenuto di Body_t.
receive
Any -> do_something(Any)
end.In questo esempio l'actor prenderà qualsiasi messaggio, quindi rimane in attesa solo quando la mailbox è vuota.
receive
{Pid, something} -> do_something(Pid)
end.In questo esempio l'actor prende (se esiste) il messaggio più vecchio che effettua il match con {Pid, something}. Rimane in attesa fintanto che la mailbox è vuota o non contiene messaggi di questo tipo.
Esempi:
Oltre che riferirci ad un processo mediante il suo pid, sono disponibili delle BIF per registrare un processo sotto un certo nome. Il nome deve essere un atomo. Quando il processo termina la registrazione viene annullata automaticamente.
register(atomo, Pid)registered(): restituisce una lista dei nomi registratiunregister(atomo)whereis(atomo): restituisce il pid registrato con il nomeatomooundefinedse il nome non è registrato
Ovviamente una volta assegnato un nome ad un processo è possibile utilizzarlo anche per inviargli un messaggio (atomo ! messaggio).
Come suggerisce il nome, sono funzioni definite come parte di Erlang. Generalmente
le BIFs forniscono interfacce verso il sistema operativo o eseguono operazioni che sarebbero
impossibili o difficili da implementare in Erlang. Alcuni esempi di BIFs sono: time, list_to_tuple, is_process_alive, spawn e statistics.
> time().
{14,59,21}
> list_to_tuple([1,2,3]).
{1,2,3}> is_process_alive(<0.99.0>).
false
% In Erlang (essendo un linguaggio funzionale), le funzioni devono sempre
% restituire qualcosa (in questo caso non mi serve quindi restituisco un
% atomo con un nome significativo in modo da dare un po' di semantica).
> spawn(fun() -> receive die -> void end end). % void è un atomo
<0.98.0>
> is_process_alive(<0.98.0>).
true
> <0.98.0> ! die.
die
> is_process_alive(<0.98.0>).
falseTutte le BIFs appartengono al modulo erlang, ma la maggior parte sono importate di default, quindi non è necessario specificare il prefisso erlang:
> statistics(wall_clock). % non serve specificare il modulo
{544554,116524}
> erlang:statistics(wall_clock).
{547095,2541}Lista completa nella documentazione ufficiale.
Esempi:
In erlang, di solito, gli errori non vengono gestiti nel processo dove l'errore viene generato. Si preferisce lasciar terminare il processo in questione e correggere l'errore in qualche altro processo.
Questo significa che è importante poter rilevare la terminazione anomala di un processo da un altro processo. Se si organizza il sistema in questo modo, quello che otteniamo è un insieme di processi di cui una parte (o tutti) monitora lo stato di altri processi.
Per realizzare tutto questo Erlang mette a disposizione diversi strumenti e concetti:
-
Se due processi P1 e P2 sono collegati e P1 termina per qualsiasi ragione, viene inviato un segnale di uscita a P2 (e viceversa).
La BIF
link/1crea un collegamento tra il processo chiamante e quello specificato. Un collegamento è bidirezionale e tra ogni coppia di processi ne può esistere al massimo uno. -
Quando un processo P termina, viene inviato un segnale di uscita a tutti i processi ad esso collegati (l'insieme dei processi collegati a P si definisce linkset).
-
Sono i processi creati con la BIF
spawn. Quando un processo normale riceve un segnale di uscita da parte di un processo che è terminato con un anomalia, esso terminerà a sua volta (se il segnale di uscita è una terminazione normale, allora non termina). -
Un processo normale diventa un processo di sistema dopo la chiamata
process_flag(trap_exit, true). Quando un processo di sistema riceve un segnale di uscita, questo viene trasformato in un messaggio{'EXIT', Pid, Why}depositato nella sua mailbox.Pidsi riferisce al processo che è terminato eWhycontiene la ragione di tale terminazione. Se il processo è terminato senza errori,Whyvalenormal. -
I monitor sono simili ai link ma unidirezionali. Supponiamo che il processo P1 stia monitorando P2. Quando P2 termina verrà inviato un messaggio
{'DOWN', MonitorRef, Type, Object, Info}a P1 (quindi se si usano i monitor non è necessario diventare un system process per gestire gli errori).
Esempi:
- link example
- monitor example
- utilizzo di exit/2
- propagazione dei segnali di errore nei processi normali
- propagazione dei segnali di errore con un system process
- semplice esempio di system process (trap_exit)