Cos'è Elixir e perché regge milioni di connessioni
Cos'è Elixir: la macchina virtuale BEAM di Erlang, i processi leggeri, la filosofia let it crash, Phoenix LiveView e i limiti reali del linguaggio.
Hai costruito una chat o un sistema di notifiche, e appena gli utenti connessi insieme crescono il server comincia a soffrire: thread che si bloccano, memoria che sale, un errore in una connessione che si porta dietro tutte le altre. Esiste una tecnologia nata esattamente per questo problema, quarant'anni fa, per le centrali telefoniche. In questo articolo trovi cos'è Elixir, perché il suo vero segreto non è Elixir stesso, e quando ha senso sceglierlo.
Cos'è Elixir
Elixir è un linguaggio funzionale creato nel 2012 che gira sulla macchina virtuale di Erlang, la BEAM: porta una sintassi moderna e strumenti comodi sopra una tecnologia progettata per sistemi che devono gestire moltissime attività contemporanee e non fermarsi mai.
L'ha creato José Valim, che veniva dal core team di Ruby on Rails. Si vede: la sintassi ricorda molto Ruby, con do ... end, parentesi facoltative e un'attenzione alla leggibilità. Ma la somiglianza è solo in superficie. Sotto, il modello di esecuzione non ha niente a che fare con Ruby.
Il segreto è la BEAM, non Elixir
Per capire Elixir devi partire da Erlang. Negli anni Ottanta Ericsson aveva un problema preciso: il software delle centrali telefoniche non poteva mai fermarsi. Né per un bug, né per un aggiornamento, né per un guasto hardware. Una centrale che si riavvia fa cadere migliaia di telefonate.
Da quel vincolo è nato Erlang, insieme alla sua macchina virtuale (la BEAM) e a un insieme di librerie chiamato OTP. Le scelte fatte allora sono ancora quelle:
- Tantissime attività contemporanee, ognuna isolata dalle altre.
- Gli errori sono normali: il sistema deve sopravvivere ai propri guasti, non evitarli tutti.
- Aggiornare senza spegnere: la BEAM permette perfino di sostituire il codice mentre il sistema gira.
Erlang funziona benissimo — WhatsApp è il caso più citato di sistema costruito su Erlang — ma la sua sintassi, derivata da Prolog, è spigolosa per chi arriva da linguaggi moderni. Elixir prende tutta quella tecnologia e le mette sopra un linguaggio piacevole da scrivere, con un gestore di pacchetti (Hex), uno strumento di build (Mix) e un sistema di macro. Il codice Elixir e quello Erlang si chiamano a vicenda senza attrito: le librerie di trent'anni sono tutte disponibili.
I processi leggeri: niente memoria condivisa
Il concetto centrale della BEAM è il processo. Attenzione: non è un processo del sistema operativo e nemmeno un thread. È un'unità di esecuzione gestita dalla macchina virtuale, che parte con pochissima memoria e si crea in microsecondi. Averne centinaia di migliaia su una sola macchina è normale.
for n <- 1..100_000 do
spawn(fn -> IO.puts("Sono il processo #{n}") end)
end
La differenza vera rispetto ai thread di Java o alle goroutine di Go è un'altra: i processi non condividono memoria. Ognuno ha il suo spazio, e l'unico modo di comunicare è mandarsi messaggi.
pid = spawn(fn ->
receive do
{:saluta, nome} -> IO.puts("Ciao #{nome}")
end
end)
send(pid, {:saluta, "Giulia"})
Le conseguenze sono enormi:
- Niente race condition classiche. Se due processi non possono toccare la stessa variabile, non possono corromperla insieme. Non servono lucchetti.
- Un processo che va in crash non sporca gli altri. Non c'è stato condiviso lasciato a metà.
- La garbage collection è per processo. Ogni processo pulisce la sua memoria per conto suo, senza fermare tutto il sistema: la latenza resta prevedibile anche sotto carico.
- Lo scheduler è preemptive. La BEAM distribuisce il tempo tra i processi e non lascia che uno solo, magari bloccato in un ciclo, monopolizzi la CPU.
In un server web Elixir, ogni connessione è un processo. Diecimila utenti connessi sono diecimila processi isolati, e uno che si comporta male non tocca gli altri novemilanovecentonovantanove.
"Let it crash": il cambio di mentalità
Questa è la parte più difficile da digerire, ed è anche la più importante.
Nella programmazione difensiva classica cerchi di prevedere ogni errore: controlli se il file esiste, se la rete risponde, se il dato ha il formato giusto, e per ogni caso scrivi una gestione. Il codice si riempie di try/catch, e comunque qualche caso ti sfugge — e quello che ti sfugge lascia il programma in uno stato strano.
La filosofia Erlang dice un'altra cosa: gestisci gli errori che sai gestire, e per tutto il resto lascia morire il processo. Qualcuno lo riavvierà in uno stato pulito e noto.
Quel qualcuno è il supervisor: un processo il cui unico lavoro è sorvegliare altri processi e riavviarli quando muoiono, secondo una strategia che decidi tu.
defmodule Contatore do
use GenServer
def start_link(iniziale),
do: GenServer.start_link(__MODULE__, iniziale, name: __MODULE__)
def incrementa, do: GenServer.cast(__MODULE__, :incrementa)
def valore, do: GenServer.call(__MODULE__, :valore)
@impl true
def init(iniziale), do: {:ok, iniziale}
@impl true
def handle_cast(:incrementa, stato), do: {:noreply, stato + 1}
@impl true
def handle_call(:valore, _da, stato), do: {:reply, stato, stato}
end
figli = [{Contatore, 0}]
Supervisor.start_link(figli, strategy: :one_for_one)
Se Contatore va in crash, il supervisor lo fa ripartire da zero. I supervisor si organizzano ad albero: un supervisor sorveglia altri supervisor, e un guasto risale solo fin dove serve.
"Let it crash" non vuol dire ignorare gli errori. Vuol dire separare il codice che fa il lavoro dal codice che gestisce i guasti, e accettare che il modo più affidabile di riprendersi da uno stato imprevisto è ricominciare da uno stato conosciuto. È lo stesso ragionamento per cui "spegni e riaccendi" risolve tanti problemi — solo che qui è progettato, automatico, e riguarda un singolo processo invece dell'intero server.
Immutabilità e pattern matching
Elixir è un linguaggio funzionale: se il paradigma ti è nuovo, trovi le basi in paradigmi di programmazione. Qui conta vedere due conseguenze pratiche.
I dati sono immutabili. Non modifichi una lista: ne ottieni una nuova. Questo si sposa perfettamente con i processi isolati, perché un messaggio inviato non può essere cambiato da nessuno dopo.
Il pattern matching è ovunque. Invece di estrarre i valori con condizioni, descrivi la "forma" che ti aspetti:
case File.read("config.json") do
{:ok, contenuto} -> elabora(contenuto)
{:error, :enoent} -> IO.puts("File non trovato")
{:error, motivo} -> IO.puts("Errore: #{inspect(motivo)}")
end
E l'operatore pipe |> rende leggibili le trasformazioni in sequenza:
" Ciao Mondo Elixir "
|> String.trim()
|> String.downcase()
|> String.split(" ")
# ["ciao", "mondo", "elixir"]
Se ti interessa il lato più rigoroso del funzionale, con un sistema di tipi statico molto potente, il confronto naturale è Haskell. Elixir è più pragmatico: tipizzazione dinamica, con un sistema di tipi graduale che sta entrando nelle versioni recenti.
Phoenix e LiveView
Il framework web di riferimento è Phoenix, che nel suo territorio — applicazioni con tante connessioni aperte — è tra i più efficienti in circolazione.
La sua funzione più interessante è LiveView: interfacce interattive in tempo reale senza scrivere JavaScript applicativo. Il browser apre una connessione WebSocket verso il server; lo stato della pagina vive in un processo Elixir; quando qualcosa cambia, il server calcola solo la differenza nell'HTML e la invia. Un contatore, un form con validazione immediata, una dashboard che si aggiorna: tutto scritto in Elixir.
Funziona proprio grazie alla BEAM: un processo per ogni utente connesso costa poco, e tenerne migliaia aperti è il caso d'uso per cui la macchina è nata.
Il limite: ogni interazione passa dal server, quindi con una connessione lenta o instabile l'esperienza ne risente. Per interfacce che devono funzionare offline o con animazioni complesse lato client, un framework JavaScript resta la scelta giusta.
Dove brilla e dove no
| Ambito | Adatto? | Perché |
|---|---|---|
| Chat, messaggistica | Molto | Un processo per connessione, isolamento dei guasti |
| Sistemi realtime, notifiche, dashboard live | Molto | Phoenix Channels e LiveView |
| Tante connessioni contemporanee (IoT, giochi multiplayer) | Molto | È il motivo per cui la BEAM esiste |
| API web classiche | Sì | Funziona bene, ma il vantaggio è meno netto |
| Calcolo numerico intensivo | No | La BEAM non è ottimizzata per la potenza di calcolo bruta |
| Data science, machine learning | Poco | Esiste Nx, ma l'ecosistema vero è in Python |
| App mobile, desktop | No | Non è il suo territorio |
Discord è il nome che si cita più spesso tra chi usa Elixir in produzione, proprio per la parte di messaggistica in tempo reale.
Errori comuni
Usare un processo come fosse un oggetto. Chi arriva dalla programmazione a oggetti tende a creare un processo per ogni entità. Un processo serve quando c'è concorrenza o stato da isolare, non per organizzare il codice: per quello bastano moduli e funzioni.
Creare un collo di bottiglia con un singolo GenServer. Un processo gestisce un messaggio alla volta. Se tutte le richieste passano da lì, hai reintrodotto la serializzazione che volevi evitare.
Fraintendere "let it crash" come "non gestire niente". Gli errori prevedibili — input non valido, utente inesistente — si gestiscono normalmente con {:ok, ...} e {:error, ...}. Il crash è per ciò che non sai come recuperare.
Sceglierlo per mode. Se il tuo progetto è un gestionale con poche decine di utenti, non stai usando nessuno dei vantaggi di Elixir e paghi tutti i costi di un ecosistema più piccolo.
In sintesi
Elixir è una sintassi moderna sopra una tecnologia collaudata: la forza viene dalla BEAM di Erlang, nata per centrali telefoniche che non potevano fermarsi.
Il modello a processi isolati cambia le regole della concorrenza. Niente memoria condivisa, comunicazione solo via messaggi, e un processo che muore non trascina gli altri.
"Let it crash" e i supervisor sono un cambio di mentalità vero: invece di prevedere ogni errore, progetti il sistema per riprendersi da solo ripartendo da uno stato pulito.
I limiti sono concreti: la comunità è piccola, in Italia le offerte di lavoro sono poche, e per il calcolo numerico o il machine learning ci sono scelte migliori. Brilla dove servono tante connessioni e tempo reale; altrove, raramente vale il cambio.
Per collocarlo tra gli altri linguaggi c'è tutti i linguaggi di programmazione.