In Elixir il segno = non assegna: confronta. Scrivere
{:ok, valore} = chiamata() significa «pretendo che il risultato sia una coppia il
cui primo elemento e' :ok, e se lo e' dammi il secondo». Se la funzione ha
restituito {:error, :timeout} il programma si ferma li', sulla riga giusta, invece
di proseguire con una variabile che contiene qualcosa di inatteso.
E' il punto da cui conviene entrare nel linguaggio se vieni da Java, C# o Python, perche' tutto il resto - il pipe operator, l'immutabilita', il modo in cui si scrivono le funzioni - e' conseguenza di questa scelta. Questo articolo attraversa quei fondamentali nell'ordine in cui si appoggiano l'uno sull'altro.
I blocchi di codice di questo articolo non sono stati eseguiti
Sulla macchina su cui e' stato scritto l'articolo non c'e' un ambiente Elixir
(elixir --version e mix non sono installati), quindi gli output
riportati nei commenti non provengono da un'esecuzione. Sono il
comportamento documentato delle funzioni usate, verificato sulla documentazione ufficiale
linkata in fondo. Dove il risultato dipende da un dettaglio non ovvio, il testo dice quale
pagina lo stabilisce. Riproducili in iex: e' il modo giusto di leggere questo
articolo.
Perche' la BEAM, e che cosa non le va attribuito
Elixir non e' un linguaggio funzionale fra tanti: e' un linguaggio che compila per la BEAM, la macchina virtuale di Erlang. Erlang nasce nei laboratori Ericsson, e nella sua tesi di dottorato Joe Armstrong data al 1986 l'inizio del lavoro sul linguaggio. Il prodotto simbolo e' il commutatore ATM AXD301, consegnato per la prima volta nel 1998, che nel 2003 contava oltre 1,7 milioni di righe di Erlang.
Il «99,9999999% di uptime» e' citato molto piu' spesso di quanto sia documentato
Il famoso dato dei «nove nove» circola come se fosse una misura. La fonte primaria e' la tesi di Armstrong, e dice qualcosa di piu' ruvido: l'unica informazione sulla stabilita' a lungo termine dell'AXD301 veniva da «una presentazione PowerPoint che mostrava alcune cifre secondo cui un cliente importante aveva fatto girare un sistema a 11 nodi con affidabilita' del 99,9999999%», e Armstrong aggiunge che come quelle cifre fossero state ottenute non era documentato.
Nello stesso capitolo racconta di aver cercato nei log le prove dei code upgrade a caldo e di non averle trovate, perche' il sistema non era strumentato per registrarle. Vale la pena saperlo per due ragioni: perche' e' corretto, e perche' la cosa interessante dell'AXD301 non e' un numero su una slide, sono le 1,7 milioni di righe in esercizio. Qui il numero resta solo con la sua provenienza attaccata.
Il percorso di questo articolo
- Installazione con
asdf, e il REPLiex - I tipi che userai davvero: atom, tuple, list, map, keyword list
- Pattern matching, dalle tuple alle teste di funzione con le guard
- Il pipe operator
|>, e la regola del primo argomento - Immutabilita', e perche' il rebinding non la viola
- Moduli, funzioni private, capture syntax
EnumcontroStream: quando si calcola
Le prime due voci sono strumentali, la terza e' il cuore: se porti a casa solo il pattern
matching hai gia' cambiato il modo in cui scrivi il codice, perche' sostituisce la maggior
parte degli if annidati con teste di funzione separate. Le ultime due sono quelle
che un principiante salta e poi paga: la differenza fra Enum e
Stream decide se il tuo programma carica in memoria un file da 2 GB o lo legge una
riga alla volta.
Installazione e primo progetto
Elixir dipende da una versione di Erlang/OTP compatibile, quindi la via meno dolorosa e' un
gestore di versioni che installi entrambi. asdf fa questo.
# I due plugin: Erlang prima, Elixir poi (Elixir ha bisogno della VM)
asdf plugin add erlang
asdf plugin add elixir
asdf install erlang 29.1.2
asdf install elixir 1.20.5-otp-29
asdf global erlang 29.1.2
asdf global elixir 1.20.5-otp-29
elixir --version
# Nuovo progetto
mix new my_app
cd my_app
# REPL con il progetto gia' compilato e caricato
iex -S mix
# Struttura generata da `mix new`
# my_app/
# |- lib/
# | \- my_app.ex modulo principale
# |- test/
# | \- my_app_test.exs test (estensione .exs: non compilato)
# |- mix.exs configurazione e dipendenze
# \- README.md
Due dettagli meritano attenzione. Il primo e' il suffisso -otp-29 nella versione di
Elixir: le build di Elixir sono compilate contro una specifica serie di OTP, e installare
1.20.5 senza suffisso su una VM di serie diversa produce avvisi o errori di
compilazione.
Il secondo e' iex -S mix, che non e' lo stesso di iex
nudo: con -S mix il REPL parte dopo aver compilato e caricato il progetto, quindi i
tuoi moduli sono gia' disponibili. Senza, ottieni una sessione vuota e nessuno dei moduli che
hai scritto.
Sulle versioni: Elixir 1.20.5 e OTP 29.1.2 sono le release pubblicate rispettivamente il 2026-10-29 e il 2026-10-14 secondo i rilasci ufficiali dei due progetti su GitHub, consultati il 2026-11-08. Sono numeri che invecchiano: controlla tu, invece di copiarli, perche' un tutorial che pinna una versione vecchia e' il modo piu' comune di perdere un pomeriggio su un errore che non ti riguarda.
I tipi che userai davvero
Elixir ha pochi tipi, e tre di questi fanno la quasi totalita' del lavoro: l'atom come etichetta, la tuple come record di posizione fissa, la map come dizionario. La keyword list sembra ridondante e non lo e'.
# Atom: una costante il cui valore e' il suo nome
:ok
:error
true # true, false e nil sono atom: :true, :false, :nil
# Numeri
42
3.14
1_000_000 # l'underscore e' solo leggibilita'
# Stringhe: binary UTF-8
"Ciao, mondo!"
"Interpolazione: #{1 + 1}" # "Interpolazione: 2"
:hello == "hello" # false: atom e stringa sono tipi diversi
# Tuple: lunghezza fissa, accesso per posizione
{:ok, "valore"}
{:error, :not_found}
# List: lista concatenata, efficiente su testa/coda
[1, 2, 3, 4, 5]
[head | tail] = [1, 2, 3] # head = 1, tail = [2, 3]
# Map: chiave-valore
%{name: "Mario", age: 35} # chiavi atom
%{"name" => "Mario", "age" => 35} # chiavi stringa
# Keyword list: lista di tuple {atom, valore}, ordinata, con duplicati ammessi
[name: "Mario", age: 35, city: "Milano"]
# e' zucchero per: [{:name, "Mario"}, {:age, 35}, {:city, "Milano"}]
# Range
1..10
1..10//2 # con passo 2: 1, 3, 5, 7, 9
La distinzione fra tuple e list e' una scelta di prestazioni, non di gusto: la tuple e' contigua
in memoria e si legge per indice in tempo costante, la list e' concatenata e per raggiungere
l'ultimo elemento deve attraversarla tutta. Per questo la convenzione dei risultati e' una
tuple: {:ok, valore} oppure {:error, motivo}. Due posizioni,
la prima sempre un'etichetta.
La differenza fra map e keyword list e' quella che confonde all'inizio. La map non ha ordine e
non ammette chiavi duplicate; la keyword list conserva l'ordine e ammette ripetizioni. Per questo
le opzioni delle funzioni sono keyword list: Repo.all(query, timeout: 5000, log: false)
e' una keyword list passata come ultimo argomento, e la sintassi senza parentesi quadre funziona
solo in quella posizione.
Il // del range con passo esiste dalla versione 1.12 del linguaggio: su una
installazione piu' vecchia e' un errore di sintassi.
Pattern matching: dove si decide tutto
= e' l'operatore di match. Il lato sinistro e' un pattern, il destro un
valore: Elixir prova a far combaciare la forma, lega le variabili che trova nel pattern e, se la
forma non combacia, solleva MatchError.
x = 42 # lega x a 42
42 = x # match riuscito: non assegna niente, verifica
# 43 = x # ** (MatchError) no match of right hand side value: 42
# Destrutturazione di tuple
{:ok, value} = {:ok, "risultato"} # value = "risultato"
# {:ok, value} = {:error, :not_found} # MatchError: :ok non combacia con :error
# case: prova i pattern in ordine, dall'alto
result = {:error, :timeout}
case result do
{:ok, value} -> IO.puts("Successo: #{value}")
{:error, :not_found} -> IO.puts("Non trovato")
{:error, reason} -> IO.puts("Errore: #{reason}")
_ -> IO.puts("Qualsiasi altro caso")
end
# stampa: Errore: timeout
# Liste: testa e coda
[first | rest] = [1, 2, 3, 4, 5] # first = 1, rest = [2, 3, 4, 5]
[a, b | remaining] = [10, 20, 30, 40] # a = 10, b = 20, remaining = [30, 40]
# Map: il match e' parziale, le chiavi in eccesso non disturbano
%{name: name} = %{name: "Mario", age: 35, city: "Milano"} # name = "Mario"
# Pin operator ^: confronta col valore attuale invece di rilegare
existing = 42
^existing = 42 # ok
# ^existing = 43 # MatchError
L'ordine delle clausole di case non e' indifferente: Elixir prende la
prima che combacia. Nell'esempio, {:error, :timeout} scivola oltre
{:error, :not_found} e si ferma su {:error, reason}, che lega
reason a :timeout. Se spostassi la clausola generica
{:error, reason} in cima, le due righe sotto diventerebbero codice morto - e il
compilatore te lo dice con un avviso di clausola non raggiungibile.
Il _ finale e' la clausola di riserva, e qui c'e' un compromesso da scegliere con
la testa: metterlo evita un CaseClauseError a runtime, ma spegne anche l'avviso che
ti avverte di aver dimenticato un caso. Quando i casi possibili sono noti e finiti - gli stati di
un ordine, i codici che la tua API puo' restituire - non mettere il
_ e' spesso la scelta migliore: preferisci accorgerti in fase di sviluppo che un
caso nuovo non e' gestito.
Sul pin operator: senza ^, scrivere existing = 43 non confronta niente,
rilega semplicemente existing a 43. E' la differenza fra «verifica che sia 43» e
«da ora vale 43», e dentro un case e' la differenza fra un confronto e un bug
silenzioso.
Lo stesso meccanismo nelle teste di funzione
Il pattern matching non vive solo dentro case: puoi scrivere piu' definizioni della
stessa funzione, ciascuna con un pattern diverso. E' qui che il linguaggio cambia il modo in cui
organizzi il codice.
defmodule HttpResponse do
def handle({:ok, %{status: 200, body: body}}), do: IO.puts("OK: #{body}")
def handle({:ok, %{status: 404}}), do: IO.puts("Not Found")
# Guard: condizione che il solo pattern non puo' esprimere
def handle({:ok, %{status: status}}) when status >= 500 do
IO.puts("Errore del server: #{status}")
end
def handle({:error, reason}), do: IO.puts("Richiesta fallita: #{reason}")
end
HttpResponse.handle({:ok, %{status: 200, body: "ciao"}}) # OK: ciao
HttpResponse.handle({:ok, %{status: 503}}) # Errore del server: 503
HttpResponse.handle({:error, :timeout}) # Richiesta fallita: timeout
defmodule Validator do
def validate_age(age) when is_integer(age) and age >= 0 and age <= 150 do
{:ok, age}
end
def validate_age(age) when is_integer(age) do
{:error, "eta' #{age} fuori dall'intervallo 0-150"}
end
def validate_age(_), do: {:error, "l'eta' deve essere un intero"}
end
In HttpResponse sono quattro funzioni con lo stesso nome e la stessa arieta': Elixir
le prova in ordine di scrittura e usa la prima il cui pattern combacia. Scritto in Java, questo
sarebbe uno switch sullo status dentro un if sul successo, con due
livelli di annidamento; qui ogni caso e' una riga indipendente che puoi leggere da sola.
La clausola when status >= 500 mostra il confine fra pattern e guard: un pattern
puo' solo descrivere una forma, quindi «status uguale a 404» si esprime col pattern, ma
«status maggiore o uguale a 500» ha bisogno di una guard. Le guard accettano solo un elenco
chiuso di funzioni - is_integer/1, i confronti, l'aritmetica - e non una qualunque
funzione tua: la pagina delle guard nella documentazione elenca quali.
In Validator l'ordine e' portante. La prima clausola accetta gli interi nell'intervallo,
la seconda cattura gli interi fuori intervallo, la terza tutto il resto. Scambiando la prima con
la seconda, ogni intero cadrebbe nella seconda e la validazione non direbbe mai
:ok. E' il tipo di errore che non produce un'eccezione: produce un programma che
rifiuta tutto.
Il pipe operator, e la regola del primo argomento
|> prende il valore a sinistra e lo passa come primo argomento
alla funzione a destra. Tutta la sua utilita' - e il suo unico limite - sta in quella parola.
# Senza pipe: si legge da dentro verso fuori, al contrario dell'ordine di esecuzione
result = Enum.sum(Enum.filter(Enum.map([1, 2, 3, 4, 5], fn x -> x * 2 end), fn x -> x > 4 end))
# Con pipe: si legge dall'alto in basso, nell'ordine in cui accade
result =
[1, 2, 3, 4, 5]
|> Enum.map(fn x -> x * 2 end) # [2, 4, 6, 8, 10]
|> Enum.filter(fn x -> x > 4 end) # [6, 8, 10]
|> Enum.sum() # 24
# Un caso concreto: la lista utenti da mostrare in una pagina
defmodule UserProcessor do
def process_active_users(users) do
users
|> Enum.filter(&active?/1)
|> Enum.sort_by(& &1.name)
|> Enum.map(&enrich_with_metadata/1)
|> Enum.take(50)
end
defp active?(%{status: :active}), do: true
defp active?(_), do: false
defp enrich_with_metadata(user) do
Map.put(user, :display_name, format_display_name(user))
end
defp format_display_name(%{name: name, city: city}), do: "#{name} (#{city})"
defp format_display_name(%{name: name}), do: name
end
users = [
%{name: "Mario", status: :active, city: "Milano"},
%{name: "Luigi", status: :inactive, city: "Roma"},
%{name: "Peach", status: :active, city: "Torino"}
]
UserProcessor.process_active_users(users)
# Luigi esce al filtro; restano Mario e Peach, ordinati per nome,
# ciascuno con :display_name aggiunto - "Mario (Milano)", "Peach (Torino)"
Confronta le due forme sulla stessa logica. Nella prima riga l'operazione che avviene per prima,
Enum.map, e' scritta in mezzo, e per capire l'ordine devi leggere dall'interno verso
l'esterno; nella pipeline l'ordine di scrittura e' l'ordine di esecuzione. Non e' una questione
estetica: e' la ragione per cui puoi commentare una riga della pipeline per escludere un passaggio
e il resto continua a compilare.
In process_active_users l'ordine dei passi ha un costo. Enum.take(50)
sta in fondo, quindi enrich_with_metadata viene applicata a tutti gli utenti
attivi, non ai 50 che mostrerai. Spostare take prima di map farebbe
meno lavoro - ma solo perche' qui l'ordinamento precede: se togliessi sort_by,
anticipare take cambierebbe quali 50 utenti ottieni, non solo quanto costa.
E' il genere di scelta che il pipe rende visibile, ed e' il motivo per cui conviene scriverlo.
Sulla sintassi &active?/1 e & &1.name: e' la capture syntax,
una scorciatoia per fn x -> active?(x) end e fn x -> x.name end.
&1 e' il primo argomento. Infine la trappola del pipe: funziona solo se il valore
che stai passando e' il primo parametro della funzione. Con String.replace/3, dove il
soggetto e' il primo argomento, |> funziona; con una funzione che si aspetta il dato
come secondo argomento, non puoi usarlo e devi scrivere una fn esplicita.
Immutabilita', e perche' il rebinding non la viola
Nessuna struttura dati in Elixir si modifica. Le funzioni che sembrano modificare restituiscono una struttura nuova e lasciano intatta quella che hanno ricevuto. E' la precondizione della concorrenza della BEAM: se nessuno puo' alterare un dato che un altro sta leggendo, non serve sincronizzare l'accesso.
# Liste
original = [1, 2, 3]
new_list = [0 | original] # [0, 1, 2, 3]
original # ancora [1, 2, 3]
# Map
user = %{name: "Mario", age: 35}
updated = Map.put(user, :city, "Milano")
# updated = %{name: "Mario", age: 35, city: "Milano"}
user # ancora %{name: "Mario", age: 35}
# Zucchero per aggiornare chiavi che esistono gia'
older = %{user | age: 36} # %{name: "Mario", age: 36}
# %{user | city: "Roma"} # KeyError: :city non esiste in user
# Rebinding: NON e' mutazione
x = 1
x = x + 1 # x ora vale 2; il numero 1 non e' cambiato,
# e' l'etichetta x che punta a un altro valore
Il passaggio da capire e' l'ultimo. x = x + 1 sembra la mutazione di sempre, e non lo
e': il valore 1 e' immutabile e resta tale, cio' che cambia e' a quale valore e'
legato il nome x. La differenza diventa concreta quando il vecchio valore e' ancora
in mano a qualcuno - un altro processo, una closure - che continuera' a vedere 1.
Le due sintassi di aggiornamento della map non sono interscambiabili, ed e' voluto.
Map.put/3 aggiunge o sovrascrive, quindi non fallisce mai; %{user | age: 36}
pretende che la chiave esista e solleva KeyError se non c'e'. La
seconda forma e' la piu' sicura quando stai aggiornando un record di cui conosci la forma, perche'
un errore di battitura nel nome della chiave diventa un'eccezione invece di una chiave nuova
aggiunta in silenzio.
Nota anche che [0 | original] mette l'elemento in testa, non in coda. Non e' una
preferenza: su una lista concatenata l'inserimento in testa e' a costo costante, l'aggiunta in
coda deve ricostruire la lista. Per questo l'idioma di Elixir e' accumulare in testa e invertire
alla fine con Enum.reverse/1, anziche' appendere.
Moduli e funzioni
Un modulo e' un contenitore di funzioni e l'unita' di compilazione. Le annotazioni che lo accompagnano - documentazione, specifiche di tipo, attributi - non sono decorazione: sono lette da strumenti che usi davvero.
defmodule MyApp.Calculator do
@moduledoc "Operazioni aritmetiche di esempio."
@doc "Somma due interi."
@spec add(integer(), integer()) :: integer()
def add(a, b), do: a + b
# Due clausole: il caso degenere prima, cosi' non serve un if
@spec divide(number(), number()) :: {:ok, float()} | {:error, String.t()}
def divide(_, 0), do: {:error, "divisione per zero"}
def divide(a, b), do: {:ok, a / b}
# defp: invisibile fuori dal modulo
defp validate_positive(n) when n > 0, do: :ok
defp validate_positive(_), do: :error
def run_examples do
double = fn x -> x * 2 end
triple = &(&1 * 3) # capture syntax
IO.puts(double.(5)) # 10 - il punto serve per chiamare una fn anonima
IO.puts(triple.(5)) # 15
[1, 2, 3, 4, 5]
|> Enum.map(double)
|> IO.inspect() # [2, 4, 6, 8, 10]
end
end
defmodule Config do
# Attributi di modulo: costanti risolte a tempo di compilazione
@max_retries 3
@base_url "https://api.example.com"
def max_retries, do: @max_retries
def base_url, do: @base_url
end
divide(_, 0) scritta prima di divide(a, b) e' l'intero
meccanismo in una riga: il caso particolare intercetta, il caso generale raccoglie il resto, e non
c'e' nessun if. Invertendo le due righe, divide(a, b) catturerebbe
anche b = 0 e otterresti un ArithmeticError invece della tupla
{:error, ...} che avevi promesso nella @spec.
Il punto in double.(5) non e' un errore di stampa. Le funzioni anonime si chiamano
con .(), quelle nominate senza punto, e la ragione e' che in Elixir
double da solo e' una variabile come un'altra: il punto dice «chiama il valore che
c'e' dentro». Togliendolo ottieni un errore di funzione non definita.
Su @spec e @doc vale la pena essere espliciti, perche' sembrano
opzionali: @doc finisce nella documentazione generata da ExDoc ed e' consultabile da
iex con h MyApp.Calculator.add; @spec e' letta da Dialyzer,
l'analizzatore statico, che segnala le chiamate incompatibili. Senza @spec il codice
compila e funziona, ma Dialyzer ha molto meno da controllare. @max_retries invece e'
risolto a compilazione: cambiarlo richiede una ricompilazione, non basta riavviare.
Enum o Stream: la domanda e' «quando si calcola»
Le due librerie espongono quasi le stesse funzioni e differiscono su un punto: Enum
calcola subito e restituisce una collezione, Stream costruisce una ricetta e non
calcola nulla finche' qualcuno non chiede il risultato.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Enum.map(numbers, fn x -> x * x end)
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Enum.filter(numbers, &(rem(&1, 2) == 0))
# [2, 4, 6, 8, 10]
Enum.reduce(numbers, 0, fn x, acc -> acc + x end)
# 55
Enum.group_by(numbers, &rem(&1, 3))
# %{0 => [3, 6, 9], 1 => [1, 4, 7, 10], 2 => [2, 5, 8]}
# Stream: niente viene calcolato finche' non arriva Enum.take
Stream.iterate(0, &(&1 + 1)) # sequenza infinita
|> Stream.filter(&(rem(&1, 2) == 0))
|> Stream.map(&(&1 * &1))
|> Enum.take(5)
# [0, 4, 16, 36, 64]
# Un file grande, una riga alla volta
File.stream!("vendite_2026.csv")
|> Stream.map(&String.trim/1)
|> Stream.filter(&String.contains?(&1, "2026-11"))
|> Enum.to_list()
L'esempio con Stream.iterate dimostra la differenza nel modo piu' netto possibile:
la sequenza di partenza e' infinita. Se sostituissi i tre Stream.
con Enum., il primo Enum.filter proverebbe a scorrere tutti gli interi e
il programma non tornerebbe mai. Con Stream, filter e map
si limitano a comporre la ricetta, e l'unica funzione che fa accadere qualcosa e'
Enum.take(5), che tira esattamente i cinque elementi richiesti e si ferma.
Il caso del file e' lo stesso meccanismo con un effetto che si misura in memoria.
File.stream!/1 non legge il file: produce uno stream di righe. Se la stessa catena
fosse scritta con File.read!/1 e Enum, l'intero file verrebbe caricato
in memoria prima del primo filtro - su un CSV da qualche gigabyte, questo e' il confine fra un
programma che gira e uno che viene ucciso dal sistema.
La scelta si riduce a quattro situazioni ricorrenti:
- Collezione piccola e di dimensione nota:
Enum - File, risposta HTTP paginata, cursore di database:
Stream - Sequenza infinita o di lunghezza non decidibile in anticipo:
Stream, obbligato - Ti serve solo un prefisso di un risultato costoso:
Streampiu'Enum.take
Il criterio che tiene insieme le quattro righe e' uno solo: quanto del lavoro puoi evitare. Se
la risposta e' «niente, li servono tutti», Enum vince sempre, e attenzione al costo
opposto - su una lista di dieci elementi Stream e' piu' lento, perche' costruire e
smontare la ricetta costa piu' del lavoro che risparmia. La riga che decide piu' spesso in un
programma reale e' la seconda, perche' e' quella in cui la dimensione della sorgente non la
scegli tu.
Che cosa sai fare adesso
Sai leggere {:ok, valore} come un pattern e non come un'assegnazione, sostituire un
if annidato con clausole di funzione e guard, costruire una trasformazione come
pipeline, e scegliere fra Enum e Stream guardando la dimensione della
sorgente. Questi quattro gesti coprono la maggior parte del codice Elixir che leggerai.
Il passo successivo e' il motivo per cui la BEAM esiste: i processi. In Elixir non sono thread del sistema operativo, non condividono memoria e si parlano per messaggi - e di questo parla BEAM e processi. Da li' si arriva a OTP e GenServer, dove lo stato mutevole torna, ma confinato dentro un processo e sorvegliato da un supervisore.
Se ti interessa il confronto con un altro modello di concorrenza, l'event loop a singolo thread di Python e' descritto in async e await: e' la strada opposta a quella della BEAM, e vederle affiancate chiarisce entrambe.
Fonti
-
Elixir - Pattern matching - la
semantica di
=come match, il comportamento del match parziale sulle map e il pin operator^. Consultata il 2026-11-08. -
Elixir - modulo
Kernel- definizione del pipe|>(passaggio come primo argomento) e l'elenco chiuso delle funzioni ammesse nelle guard. Consultata il 2026-11-08. -
Elixir - modulo
Stream- la definizione di enumerabile lazy e quali funzioni forzano il calcolo. Consultata il 2026-11-08. - Joe Armstrong, «Making reliable distributed systems in the presence of software errors» (2003) - il 1986 come inizio del lavoro su Erlang, l'AXD301 consegnato nel 1998 con oltre 1,7 milioni di righe, e il paragrafo del capitolo 8.5 da cui viene la citazione sul 99,9999999%. PDF, consultato il 2026-11-08.
- Erlang/OTP - Design Principles - il quadro in cui si inseriscono processi, behaviour e supervisione, ripreso negli articoli successivi della serie. Consultata il 2026-11-08.
-
Versioni - Elixir 1.20.5 e OTP 29.1.2 vengono dagli elenchi di release dei
due progetti su GitHub (
elixir-lang/elixireerlang/otp), interrogati il 2026-11-08: date di pubblicazione 2026-10-29 e 2026-10-14.
Le prime tre voci sono la documentazione del linguaggio, ed e' la fonte da preferire per qualunque dubbio su una funzione, perche' viene versionata insieme al codice. La quarta e' l'unica che vale la pena leggere per intero anche senza scrivere Erlang: e' il documento che spiega perche' la tolleranza ai guasti e' una proprieta' dell'architettura e non una libreria, ed e' anche la fonte che smonta il numero piu' citato di questo ecosistema.

Commenti
Caricamento commenti...
Accedi per commentare