Waarom Elixer in 2026

Elixir is niet "zomaar een functionele taal". Het is gebouwd op de Erlang's BEAM Virtual Machine, een systeem ontworpen in de jaren tachtig door Ericsson om miljoenen gelijktijdige telefoonverbindingen te beheren met een uptime van 99,9999999%. Elixir brengt op deze basis een moderne syntaxis, de Mix/Hex-toolchain, en Phoenix Framework – een van de best presterende webframeworks gedocumenteerd in de sector.

Het functionele paradigma van Elixir is geen esthetische keuze: dat is het wel wat fouttolerantie en massale gelijktijdigheid mogelijk maakt van BEAM. Om OTP en GenServer (latere artikelen) te begrijpen, moet u dit eerst doen deze basisbeginselen onder de knie te krijgen.

Wat je gaat leren

  • Installatie en configuratie met Mix, IEx (interactief REPL)
  • Basistypen: atoom, tupel, lijst, kaart, trefwoordenlijst
  • Patroonmatching: de krachtigste functie van Elixir
  • Leidingbediening |>: Leesbare functiesamenstelling
  • Onveranderlijkheid: Omdat je een variabele niet kunt veranderen
  • Functies: benoemde, anonieme en hogere-orde functies
  • Modules: Code organiseren in Elixir

Installatie en configuratie

# Installazione su Linux/Mac (via asdf, raccomandato)
asdf plugin add erlang
asdf plugin add elixir

asdf install erlang 26.2.5
asdf install elixir 1.16.3

asdf global erlang 26.2.5
asdf global elixir 1.16.3

# Verifica
elixir --version
# Erlang/OTP 26 [erts-14.2.5] [...]
# Elixir 1.16.3 (compiled with Erlang/OTP 26)

# Crea un nuovo progetto
mix new my_app
cd my_app

# Avvia il REPL interattivo
iex -S mix

# Struttura progetto generata
# my_app/
# ├── lib/
# │   └── my_app.ex        (modulo principale)
# ├── test/
# │   └── my_app_test.exs  (test)
# ├── mix.exs              (configurazione + dipendenze)
# └── README.md

Basistypen en gegevensstructuren

# IEx: esplora i tipi di Elixir

# Atoms: costanti identificate dal loro nome
:ok
:error
:hello
true    # equivale a :true
false   # equivale a :false
nil     # equivale a :nil

# Integers e floats
42
3.14
1_000_000    # underscore per leggibilita'

# Strings: binary UTF-8
"Ciao, mondo!"
"Linea 1\nLinea 2"
"Interpolazione: #{1 + 1}"   # "Interpolazione: 2"

# Atoms binari vs String
:hello == "hello"    # false - tipi diversi!
:hello == :hello     # true - stessa identita'

# Tuple: sequenza fissa di lunghezza nota
{:ok, "valore"}
{:error, :not_found}
{1, 2, 3}

# Pattern comune: tagged tuple per risultati
# {:ok, value} oppure {:error, reason}

# List: linked list (efficiente per head/tail)
[1, 2, 3, 4, 5]
["mario", "luigi", "peach"]
[head | tail] = [1, 2, 3]   # Pattern matching!
# head = 1, tail = [2, 3]

# Map: key-value store
%{name: "Mario", age: 35}      # Atom keys (piu' comune)
%{"name" => "Mario", "age" => 35}  # String keys

# Keyword list: list di tuple {atom, value} (ordinate)
[name: "Mario", age: 35, city: "Milano"]
# Equivale a: [{:name, "Mario"}, {:age, 35}, {:city, "Milano"}]
# Usata per opzioni di funzione

# Range
1..10         # Range inclusivo
1..10//2      # Step 2: [1, 3, 5, 7, 9]

Patroonaanpassing: de Elixir-kern

In Elixer, = het is geen opdracht: het is een operatie van overeenkomst. De linkerkant is "aangepast" aan de rechterkant. Als de match slaagt, zijn de variabelen in het patroon gebonden aan de overeenkomstige waarden. Als het niet lukt, wordt a geraised MatchError.

# Pattern matching: le basi

# Binding semplice
x = 42       # x viene legata a 42
42 = x       # OK: x e' 42, il match succeede
# 43 = x     # MatchError: 43 != 42

# Tuple destructuring
{:ok, value} = {:ok, "risultato"}
# value = "risultato"

{:ok, value} = {:error, :not_found}
# MatchError! :ok != :error

# Case expression: match multiplo
result = {:error, :timeout}

case result do
  {:ok, value} ->
    IO.puts("Successo: #{value}")

  {:error, :not_found} ->
    IO.puts("Non trovato")

  {:error, reason} ->
    IO.puts("Errore generico: #{reason}")

  _ ->
    IO.puts("Fallback: qualsiasi altro caso")
end
# Output: "Errore generico: timeout"

# Lista head/tail
[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 matching (partial match: extra keys sono ok)
%{name: name, age: age} = %{name: "Mario", age: 35, city: "Milano"}
# name = "Mario", age = 35

# Pin operator ^: usa il valore corrente, non rebind
existing = 42
^existing = 42   # OK: match con il valore corrente di existing
# ^existing = 43  # MatchError
# Pattern matching in function definitions
defmodule HttpResponse do
  # Diverse implementazioni per pattern diversi
  def handle({:ok, %{status: 200, body: body}}) do
    IO.puts("Success: #{body}")
  end

  def handle({:ok, %{status: 404}}) do
    IO.puts("Not Found")
  end

  def handle({:ok, %{status: status}}) when status >= 500 do
    IO.puts("Server Error: #{status}")
  end

  def handle({:error, reason}) do
    IO.puts("Request failed: #{reason}")
  end
end

# Uso
HttpResponse.handle({:ok, %{status: 200, body: "Hello"}})  # Success: Hello
HttpResponse.handle({:ok, %{status: 404}})                  # Not Found
HttpResponse.handle({:error, :timeout})                     # Request failed: timeout

# Guards (when clause): condizioni aggiuntive
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, "Age #{age} out of valid range (0-150)"}
  end

  def validate_age(_) do
    {:error, "Age must be an integer"}
  end
end

Pipe Operator: samenstelling van transformaties

De pijpoperator |> geeft het resultaat van de vorige uitdrukking door als het eerste argument van de volgende functie. Transformeer geneste oproepen in een leesbare lineaire volgorde – de code drukt een pijplijn uit van transformaties in de volgorde waarin ze plaatsvinden.

# Senza pipe: nidificazione profonda (leggibilita' scarsa)
result = Enum.sum(Enum.filter(Enum.map([1, 2, 3, 4, 5], fn x -> x * 2 end), fn x -> x > 4 end))

# Con pipe: pipeline leggibile dall'alto in basso
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

# Esempio reale: processamento di una lista di utenti
defmodule UserProcessor do
  def process_active_users(users) do
    users
    |> Enum.filter(&active?/1)           # Solo utenti attivi
    |> Enum.sort_by(& &1.name)            # Ordina per nome
    |> Enum.map(&enrich_with_metadata/1)  # Aggiungi metadata
    |> Enum.take(50)                      # Top 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})"
  end
  defp format_display_name(%{name: name}) do
    name
  end
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)
# [
#   %{name: "Mario", status: :active, city: "Milano", display_name: "Mario (Milano)"},
#   %{name: "Peach", status: :active, city: "Torino", display_name: "Peach (Torino)"},
# ]

Onveranderlijkheid: gegevens die nooit veranderen

In Elixir zijn datastructuren onveranderlijk: je kunt een lijst niet wijzigen, een bestaande kaart of tupel. Functies retourneren altijd nieuwe structuren. Dit elimineert een hele reeks bugs (bijwerkingen, gedeelde veranderlijke status) en dat maakt de concurrentie van BEAM zo robuust.

# Immutabilita': le operazioni restituiscono nuovi dati

# Liste
original = [1, 2, 3]
new_list = [0 | original]   # Prepend: [0, 1, 2, 3]
original                     # Invariato: [1, 2, 3]

# Map: non puoi modificare, ottieni una nuova map
user = %{name: "Mario", age: 35}
updated_user = Map.put(user, :city, "Milano")
# updated_user = %{name: "Mario", age: 35, city: "Milano"}
user                # Ancora %{name: "Mario", age: 35}

# Syntactic sugar per update map
updated = %{user | age: 36}   # Aggiorna solo age
# %{name: "Mario", age: 36}
# NOTA: questa sintassi fallisce se la chiave non esiste

# Rebinding: le variabili possono essere riassegnate nello stesso scope
x = 1
x = x + 1   # x e' ora 2 (ma il valore 1 non e' cambiato)
# Questo NON e' mutazione: e' binding di x a un nuovo valore

# Con il pin operator, previeni il rebinding accidentale
y = 42
case some_value do
  ^y -> "Uguale a 42"  # Match solo se some_value == 42
  _ -> "Diverso"
end

Modules en functies

# Definizione moduli e funzioni
defmodule MyApp.Calculator do
  @moduledoc """
  Modulo di esempio per operazioni aritmetiche.
  """

  # Funzione pubblica: doc + type spec
  @doc "Somma due numeri interi."
  @spec add(integer(), integer()) :: integer()
  def add(a, b), do: a + b

  # Funzione con guards
  @spec divide(number(), number()) :: {:ok, float()} | {:error, String.t()}
  def divide(_, 0), do: {:error, "Division by zero"}
  def divide(a, b), do: {:ok, a / b}

  # Funzione privata (non accessibile fuori dal modulo)
  defp validate_positive(n) when n > 0, do: :ok
  defp validate_positive(_), do: :error

  # Funzioni anonime
  def run_examples do
    double = fn x -> x * 2 end
    triple = &(&1 * 3)   # Capture syntax: shorthand per fn

    IO.puts(double.(5))   # 10  (nota il . per chiamare fn anonima)
    IO.puts(triple.(5))   # 15

    # Higher-order functions
    [1, 2, 3, 4, 5]
    |> Enum.map(double)    # Passa funzione anonima come argomento
    |> IO.inspect()        # [2, 4, 6, 8, 10]
  end
end

# Uso
MyApp.Calculator.add(3, 4)      # 7
MyApp.Calculator.divide(10, 2)  # {:ok, 5.0}
MyApp.Calculator.divide(10, 0)  # {:error, "Division by zero"}

# Module attributes: costanti compile-time
defmodule Config do
  @max_retries 3
  @base_url "https://api.example.com"
  @supported_currencies [:EUR, :USD, :GBP]

  def max_retries, do: @max_retries
  def base_url, do: @base_url
end

Enum en Stream: verzamelingsverwerking

# Enum: operazioni eager su liste (calcola subito)
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]  -- solo pari

Enum.reduce(numbers, 0, fn x, acc -> acc + x end)
# 55  -- somma totale

Enum.group_by(numbers, &(rem(&1, 3)))
# %{0 => [3, 6, 9], 1 => [1, 4, 7, 10], 2 => [2, 5, 8]}

# Stream: operazioni lazy (calcola solo quando necessario)
# Utile per collection grandi o infinite
result =
  Stream.iterate(0, &(&1 + 1))    # Lista infinita: 0, 1, 2, 3, ...
  |> Stream.filter(&(rem(&1, 2) == 0))  # Solo pari (lazy)
  |> Stream.map(&(&1 * &1))             # Quadrati (lazy)
  |> Enum.take(5)                        # Prendi i primi 5 (trigger computation)
# [0, 4, 16, 36, 64]

# Stream da file: legge una riga alla volta (memory-efficient)
# File.stream!("large_file.csv")
# |> Stream.map(&String.trim/1)
# |> Stream.filter(&String.contains?(&1, "2026"))
# |> Enum.to_list()

Conclusies

Het functionele paradigma van Elixir is geen beperking: het is het de basis die al het andere mogelijk maakt. Patroonaanpassing elimineren geneste voorwaardelijke takken. De pijpoperator geeft de transformaties weer van gegevens die leesbaar zijn als proza. Onveranderlijkheid zorgt ervoor dat de functies hebben geen verborgen bijwerkingen. Deze principes, gecombineerd met de BEAM VM, zij maken Elixir zo geschikt voor gedistribueerde systemen hoge beschikbaarheid.

Aankomende artikelen in de Elixir-serie

  • Artikel 2: BEAM en processen: enorme gelijktijdigheid zonder gedeelde threads
  • Artikel 3: OTP en GenServer: gedistribueerde status met ingebouwde fouttolerantie
  • Artikel 4: Supervisor Trees - Systemen ontwerpen die nooit sterven