Mappa Via Marconi 20, Bussolengo (VR)
Email info@devinterface.com

Introduzione a Elixir Parte 1

logo elixir

Indice

Negli ultimi anni, il mondo dello sviluppo software ha visto emergere numerose tecnologie innovative, ma poche hanno catturato l'attenzione come Elixir. Questo linguaggio di programmazione, costruito sulla solida base di Erlang e ideato per sfruttare al massimo i moderni sistemi multicore, sta rivoluzionando il modo in cui pensiamo allo sviluppo di applicazioni scalabili e fault-tolerant.

Anche noi di DevInterface non siamo rimasti indifferenti a Elixir. Proprio quest'anno, dopo averne valutato attentamente i benefici e le potenzialità offerte, abbiamo deciso di integrare questa tecnologia all'interno del nostro stack di sviluppo. Questa scelta non è stata dettata solo dalla curiosità, ma da una precisa volontà di migliorare le performance, l'affidabilità e la manutenibilità delle soluzioni che offriamo ai nostri clienti.

In questa serie di articoli, vi guideremo attraverso un percorso che vi permetterà di comprendere le caratteristiche fondamentali di Elixir, le ragioni che ci hanno spinto a adottarlo e come potete iniziare a utilizzarlo nei vostri progetti. Partiamo, quindi, con una panoramica su cosa rende Elixir così speciale.

 

Introduzione a Elixir

Quando è stata l'ultima volta che hai usato Whatsapp o Discord? Tutte e due le applicazioni girano sulla stessa macchina virtuale. Per essere un po' più precisi, Discord è alimentato da Elixir e Whatsapp da Erlang. 

Elixir è un linguaggio di programmazione funzionale creato da José Valim nel 2011. È costruito sopra Erlang, un linguaggio sviluppato da Ericsson negli anni '80 per supportare applicazioni distribuite, fault-tolerant e in tempo reale. Sia Elixir che Erlang sono compilati ed eseguiti sulla macchina virtuale Erlang, chiamata "BEAM".

Cosa c'è di così bello in Elixir?

  1. Funzionale
  2. Immutabilità
  3. Scalabilità
  4. Tolleranza ai guasti

Innanzitutto, è un linguaggio di programmazione funzionale e quindi facilita la scrittura di codice più conciso, leggibile e facile da mantenere. In secondo luogo, supporta l'immutabilità per impostazione predefinita. Quest'ultimo fattore è importante, perché ci permette di avere un'immensa scalabilità. Dato che tutti i tipi di dati sono immutabili, non c'è possibilità che thread, altri oggetti o funzioni modifichino accidentalmente i valori. Ciò si traduce in una preservazione dello stato che rende il sistema molto più scalabile. L'aspetto finale più potente di Elixir è dato dalla sua tolleranza ai guasti. Supponiamo che uno qualsiasi degli attori o dei processori si guasti: può essere automaticamente rianimato con il proprio stato precedente, grazie al robusto sistema di supervisione ereditato da Erlang.

Perché abbiamo adottato Elixir a DevInterface?

Abbiamo scelto di adottare Elixir per diversi motivi chiave:

  1. Affidabilità: la tolleranza ai guasti e la robustezza ereditate da Erlang rendono Elixir ideale per costruire applicazioni critiche che devono essere sempre disponibili.
  2. Scalabilità: la capacità di Elixir di gestire milioni di processi leggeri simultaneamente permette di costruire applicazioni altamente scalabili, pronte a crescere con le esigenze del business.
  3. Produttività: la sintassi moderna e le funzionalità avanzate di Elixir aumentano la produttività degli sviluppatori, rendendo il processo di sviluppo più efficiente e piacevole.

Con queste caratteristiche, Elixir si presenta come una scelta eccellente per costruire applicazioni moderne, scalabili e resilienti. Vediamo ora in dettaglio le sue caratteristiche. 

 

Programmazione funzionale

La programmazione funzionale significa che l'intero programma è composto da diverse funzioni. Immagina che f(x) sia una funzione. Se riceve un input, per esempio una X, la funzione trasformerà i dati da X. Questi dati saranno trasformati in qualcosa chiamato Y.
Ecco cosa farà la tua funzione. La funzione accetta sempre una sorta di argomento, trasformerà i dati e restituirà sempre un qualche tipo di dato. Sebbene possa sembrare molto semplice, non lo è affatto. Infatti, ci sono molte cose che stanno dietro alla creazione di funzioni pure.

Cosa rende dunque la programmazione funzionale così unica?

  1. Assenza di classi
  2. Dati immutabili
  3. Assenza di cicli for (for loops)

In primis, non abbiamo classi o oggetti all'interno della programmazione funzionale. Inoltre, tutti i tipi di dati sono immutabili, quindi è come lavorare con le costanti nel programma. Ora ti starai probabilmente chiedendo: perché abbiamo bisogno di tipi di dati o costanti immutabili? Perché in questo modo lo stato rimane lo stesso e i dati possono essere copiati e distribuiti molto più facilmente, consentendo di creare sistemi immensamente scalabili utilizzando la programmazione per funzioni. La presenza di dati immutabili ci porta a un terzo aspetto: non ci sono cicli for. Facciamo un esempio. Supponiamo di avere la variabile I uguale a 0, I (che è minore della lunghezza) e I più più. A ogni iterazione, la variabile I aumenta, ovvero i dati mutano continuamente, ma questo per noi non è possibile perché abbiamo tipi di dati immutabili. La programmazione funzionale e Elixir utilizzano la ricorsione come mezzo per eseguire i loop.  

 

La ricorsione

Recursione rappresentazione grafica

Ogni volta che eseguiamo ripetutamente lo stesso compito, possiamo parlare di ricorsione. Nell'immagine, si vede un compito che si richiama continuamente: questo è un classico esempio di ricorsione.

Facciamo un esempio. Organizzi una festa e vuoi invitare tutti i tuoi amici. Tuttavia, i tuoi amici conoscono altre persone che potrebbero venire alla festa. Puoi pensare a questo processo come ad una ricorsione. Tu inviti il tuo amico Paolo e gli chiedi di invitare i suoi amici. Paolo invita Marica e Giacomo. Chiedi a Marica di invitare i suoi amici e Marica invita Camilla e Martina. Chiedi a Giacomo di invitare i suoi amici e Giacomo invita Elia e Nicola. Ogni persona continua ad invitare i propri amici finché non ci sono più persone da invitare. La ricorsione in questo esempio infatti continua e si conclude soltanto quando sono stati invitati tutti e non rimane più nessuno da aggiungere alla lista degli invitati. 

In parole semplici, la ricorsione è un modo per iterare le operazioni facendole girare in loop. L'idea di base è creare una funzione che si richiama da sola finché non raggiunge una condizione finale, a quel punto la funzione smette di richiamarsi e la ricorsione si interrompe. È fondamentale pensare fin dall'inizio a quale sarà lo stato finale desiderato.

 

Pattern Matching:

Se diciamo a = 1, molto probabilmente dirai che "a" è una variabile e che il valore 1 viene assegnato alla variabile "a". È corretto, ma non è quello che succede nella programmazione funzionale. 

Se dico che a = 1, dovrebbe essere vera anche la seguente affermazione: 1 = a. "=" non è l'operatore di assegnazione, ma l'operatore di corrispondenza e ciò che fa è semplicemente far corrispondere il lato destro al lato sinistro. Questo schema si vedrà spesso in Elixir ed è chiamato anche "pattern matching". Ogni volta che vedi un'istruzione come a = 1 ricorda che stiamo semplicemente cercando di far corrispondere il lato destro con il lato sinistro. 

Facciamo un altro esempio in cui ci concentriamo sulla parte a destra e quella a sinistra: [a, a] = [1, 1] (la parentesi graffa indica una lista). Il pattern a destra (due valori) è lo stesso di quello a sinistra (due variabili). Il valore 1 è ora legato alla variabile a, così come per la seconda variabile "a" il valore contenuto è 1. 

Andiamo a vedere cosa succede se ora digitiamo [a, a] = [1, 2]. In questo caso riceviamo un messaggio di errore "no match on the right hand side". Non abbiamo una corrispondenza. Il valore 1 è legato alla variabile "a" e ancora una volta la seconda variabile è "a", ma questa volta il valore all'interno è 1 e a destra il valore è 2, quindi 2 non è uguale a 1 ed è per questo che i due lati non corrispondono e si ottiene un errore. 

Facciamo un altro step digitando [a, b] = [1, 2]. In questo caso otteniamo una corrispondenza perfetta perché il valore 1 è legato alla variabile "a" e il valore 2 è legato alla variabile "b". 

Come abbiamo detto poco fa, se vogliamo pensare in termini di pattern matching, vogliamo sempre che il lato destro sia uguale al lato sinistro. 

 

Immutabilità in Elixir

Come abbiamo visto nella sezione precedente, se digitiamo [a, a] = [1, 2] riceviamo un errore di corrispondenza. Questo avviene perché tutti i tipi di dato in Elixir sono immutabili: non possiamo assegnare casualmente nuovi valori a una variabile esistente in Elixir. Se ti stai chiedendo perché i dati sono immutabili, la causa principale è la scalabilità. 

a = 1 in questo esempio, abbiamo un operatore match e il valore 1 è legato alla variabile "a". Eppure, se dico a = 2 non avremo un errore. Come è possibile se i nostri tipi di dati sono immutabili in Elixir? Ogni volta che abbiamo una variabile sul lato sinistro del nostro operatore di corrispondenza, Elixir pensa che vogliamo legare il nuovo valore alla variabile a sinistra. Ecco perché abbiamo il nuovo valore al posto di a che è 2, ma se non vogliamo questo comportamento possiamo usare un operatore pin, quindi possiamo dire che l'operatore pin e a è uguale a 2: ^a = 3. Questa volta otteniamo un errore di corrispondenza che dice "nessuna corrispondenza del valore di destra: 3". Questo perché all'interno di "a" abbiamo il valore 2 e sul lato sinistro stiamo cercando di far corrispondere il valore 3.  Ora, se proviamo a fare il contrario e a dire che 3 = a, otterremo di nuovo un errore che dice "no match of right hand side value: 2". 

Questi esempi ti fanno capire che essendo i nostri dati immutabili, possono essere facilmente copiati tra i vari processi e non dobbiamo preoccuparci se un'altra risorsa, un thread o un processo sta modificando o cercando di mutare i nostri dati. 

 

Actor model in Elixir

Uno dei punti di forza di Elixir è il suo utilizzo dell'Actor Model per la gestione della concorrenza. L'Actor Model è un paradigma di programmazione che tratta gli "actor" come le unità fondamentali di calcolo concorrente. In poche parole, un attore è un'unità concorrente isolata: riceve qualcosa, elabora qualcosa e restituisce qualcosa. Ogni actor opera in modo isolato, senza condividere lo stato con altri actor, comunicando solo tramite il passaggio di messaggi. 

In Elixir, gli actor sono implementati tramite i processi leggeri (lightweight processes) del runtime di Erlang che non sono quelli del sistema operativo. Si possono immaginare come dei thread virtuali. In un dato momento, possono esserci milioni di processi in esecuzione. È qui che entra in gioco l'immutabilità dei dati. Possiamo avere milioni di copie dei nostri dati e distribuirle tra vari attori non solo sulla nostra macchina locale, ma anche su un cluster globale di server diversi e distribuire il carico orizzontalmente in tutto il mondo.

 

Installare Elixir

Per installare Elixir, vai sulla sezione dedicata del sito e clicca "Installa". Se usi Windows vai alla sezione Windows e scarica l'installatore apposito seguendo le istruzioni. Se utilizzi un Mac puoi usare Brew per installare Elixir. Una volta che hai installato Elixir apriamo il terminale. Possiamo entrare digitando IEX e cancellare il terminale digitando clear. 

Ora che hai installato Elixir e hai familiarizzato con le sue caratteristiche principali, sei pronto per esplorare questo potente linguaggio di programmazione. Nel prossimo articolo, entreremo nel dettaglio di Elixir e vedremo alcuni esempi pratici.