Erlang & la resilienza con pattern matching e processi

Cosa ci insegna Erlang sulla resilienza, in un periodo in cui ce ne è molto bisogno?

Erlang è un linguaggio open source nato nel 1986 alla Ericsson, di tipo funzionale, concorrente,  dinamicamente tipato, general purpose e soprattutto costruito per essere "resiliente" sia dai primi giorni della sua nascita.

E' il motore dietro RabbitMQ e CouchDB, e nell'ultima versione ha un nuovo Just In Time Compiler (JIT) che migliora le performance.

Mentre Java sta cambiando pelle, aggiungendo lambda, classi immutabili ecc, Erlang implementa già molti di questi costrutti in modo molto elegante, e il fatto che il linguaggio abbia retto alla prova del tempo ne è un ulteriore conferma; scrivo questo articolo per darvi un assaggio di queste funzionalità e per spingervi a scoprirlo, forte anche del fatto che un container docker con quattro click ve lo installa, senza troppa fatica (c'è una immagine ufficiale sull'hub di docker).

Se volete un'idea veloce del linguaggio, questo link fa al caso vostro.

In soldoni, Erlang si fonda sul concetto di funzioni ricorsive, ma rispetto a Lisp ha una sintassi un po' meno "parentesizzomane", per così dire, e soprattutto ha un trucchetto per rimuovere i side effect senza farti diventare scemo; seguitemi e saranno i vostri dieci minuti meglio spesi della giornata.

I linguaggi funzionali come Lisp sono basati sui principi matematici che ci insegnano a scuola. Mi spiego meglio. In matematica l'espressione

x=x+1

è una equazione. Nei linguaggi imperativi (come Basic) è una assegnazione di variabile, cioé a sinistra c'è una scatola e a destra c'è una espressione. Questa cosa, che va per la maggiore in tutti i linguaggi general puropose (C/Java, ecc) ha due conseguenze:

  1. Spiegare questo concetto ai nuovi arrivati è difficile
  2. Crea i cosiddetti "side effect", cioé può succedere che una variabile venga modificata in modo non tanto ovvio da un assegnamento fatto chissà dove nel codice (tipicamente un milione di righe prima...).
Il punto (2) ha creato vari problemi, ed è stato calmierato dai linguaggi ad oggetti con l'incapsulamento dello variabili in un concetto di stato. Inutile dire che i side effect, nelle loro varie forme, sono ancora uno degli errori che come consulente trovo tra i più difficili da evitare, trovare e gestire.

Se però voi aveste un linguaggio che non può avere side effect, potete sbarazzarvi di molti di questi problemi; è quello che fanno Lisp e tutti i linguaggi basati su funzioni Lambda. Questo tipo di isolamento inoltre consente un enorme parallelismo dei dati (che in questo mondo fatto di cellulari scoppiettanti è diventata una necessità imprescindibile), come vedremo a breve.

Il problema è che vivere senza variabili ti porta a costrutti un po' complicati, e quindi il problema (1) cioé insegnare ai nuovi arrivati rimane.

Erlang ha una idea geniale: le variabili ci sono ma si possono assegnare solo una volta, e sono sostanzialmente dei "binding".

E vengono scritti con la maiuscola, per distinguerle subito nel codice.

Inoltre, è possibile effettuare un "pattern matching" per assegnarle. Vediamo velocemente.

Creare una struct che contiene diversi valori si fa con una tupla:

% point è una "parola" chiamta anche "atomo"
Point = {point, 10, 45}.
% Per ottenere un valore, "destrutturo" Point con un pattern match
{point, X, Y} = Point. 
% ora X vale 10 e Y vale 45

Se il primo campo di Point non è "point", l'assegnazione (che è anche un pattern matching) va in errore.

Si può dare un nome alle strutture dati chiamandoli "record", anche se si tratta di un piccolo hack a compile time:

% Creare un file chiamato test.hrl  con il seguente contenuto
-record(pointrec, {
  x = 0,  % Default value
  y = 0
}).
% Dalla shell di Erlang:
% Uso del pointrec
rr("test.hrl").
Point1 = #pointrec{x=-1, y=1}.
% Aggiornamento: non posso riassegnare per cui a destra modifico e a sinistra mi dà il risultato
PointRecModificato = Point1#pointrec{x=1}.

Come ci ricorda Francesco Cesarini in questo articolo

I record sono stati aggiunti nel 1995 e, se c’e’ qualcosa di orrendo, sono proprio loro. Il problema e’ di origine storica. Nel 1995, non potevano aggiungere datatypes nuovi, visto che i bits nella variabile che denotavano il datatypes nella macchina virtuale erano stati tutti usati. L’unica soluzione era di convertire i records in tuple. Quando poi il problema dei bits e’ stato risolto, i record non sono mai stati ripuliti. La buona notizia e’ che a partite dalla release R17 sono stati aggiunti i records “dinamici”, chiamati maps. [...]
Personalmente aborro i record, vivo bene con le mappe dinamiche e la libreria OTP si sta adeguando. Le mappe hanno la medesima sintassi dei record ma sono un altro vivere.

Il secondo ingrediente di Erlang è il pattern matching, il modo cioé con cui posso fare un assegnamento o un confronto. Questi due concetti in Erlang sono ben  fusi, e il risultato è che potete smettere di usare gli if....

La classica funzione di fibonacci può essere descritta spiegando al sistema come gestire tre casi:

fib(0) -> 1;
fib(1) -> 1;
fib(N) when N > 1 -> fib(N-1) + fib(N-2).

Questo modo di ragionare ricorda un po' il Prolog (difatti il primo compilatore di Erlang era in Prolog....) ma è piuttosto pervasivo per cui anziché fare degli switch (case) o degli if, posso elencare i casi direttamente nella dichiarazione della funzione, stando attento all'ordine (che è importante).

Per cui le funzioni sono entità isolate, descritte in modo dichiarativo senza punti di contatto. Per far evolvere lo stato di una iterazione si usa la ricorsione di coda (come nel caso di fibonacci più sopra).

Questo ci porta al terzo ingrediente di Erlang, i processi

La funzione è intimamente legata al concetto di processo, nel senso che quello che in OOP sono oggetti dinamici che evolvono in modo concorrente, in erlang sono processi concorrenti con uno stato "fissile". Ogni processo ha una "mailbox" in cui riceve in modo asincrono dei messaggi e da cui li può pescare usando il pattern maching.

Erlang garantisce sempre l'ordine di arrivo dei messaggi (cosa non banale) e soprattutto i messaggi vengono fisicamente copiati nella mailbox destinazione; questo garantisce che anche lo spazio di memoria di ogni processo sia isolato.

Per cui cosa succede quando un processo va in crash? Molto banalmente, viene rilanciato con i medesimi input, poiché non possiede uno stato che non possa non essere riprodotto, almeno dal punto di vista del linguaggio.

Osservate che questo non è possibile farlo con un linguaggio imperativo a cui si aggiunga il concetto di Thread (e questo vale aimé anche per linguaggi moderni come Java, C# e temo anche Go) poiché al lancio di una eccezione lo stato del sistema non è isolato in modo tale da poterlo riprodurre con efficiacia.

Si noti infine come questo modello non porti a un linguaggio difficile da usare per un neofita (ho ripreso in mano Erlang tre volte negli ultimi anni, e sempre senza troppo sforzo; non male per un linguaggio che ha 35 anni d'età...).

Conclusioni e tool a supporto

I linguaggi funzionali moderni hanno tentato di aggiungere una tipizzazione forte, con alterne fortune, poiché  tali approcci tendono a creare un sistema difficile da usare (benché magari ancora più forte dal punto di vista concettuale)... si Scala,  Rust & Haskell, sto parlando con voi...

Erlang ha comunque una serie di tool di supporto come

  1. Dyalizer (DIscrepancy AnaLYZer for ERlang) che trova discrepanze tra le dichiarazioni e l'uso che si fa del codice
  2. typer che consente di annotare i tipi (approccio seguito da Python)