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.7
Gli 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ù chiaro
Le tuple in Erlang sono eterogenee.
> {"a", "b"}.
{"a","b"}
> {}.
{}
> {colore, {120, 255, 80}}.
{colore,{120,255,80}}
> {{1,2},3}=={1,{2,3}}.
false
Possiamo 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.
20
Alcune 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]
ok
Concatenazione 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:
~n
new line~p
pretty print~s
stampa una stringa senza virgolette~w
stampa 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]
ok
L'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.
9
I 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.
30
Vengono 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).
true
Esempi:
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 sequence
Una 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 % guard
Una 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:
?FILE
nome del file?MODULE
nome del modulo?LINE
numero 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
210
Esempi:
- 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 qualsiasiQualifier
può 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]
end
if
Guard_1 -> body_1;
...
Guard_n -> body_n [;]
[true -> else_body]
end
Esempi:
Erlang mette a disposizione tre funzionalità di base per realizzare la concorrenza:
spawn
la funzione built-in (BIF - built-in function) per creare nuovi actors!
l'operatore per inviare un messaggio ad un actorreceive
un 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 nomeatomo
oundefined
se 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>).
false
Tutte 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/1
crea 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.Pid
si riferisce al processo che è terminato eWhy
contiene la ragione di tale terminazione. Se il processo è terminato senza errori,Why
valenormal
. -
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)