Dlaczego Eliksir w 2026 roku

Elixir nie jest „tylko kolejnym językiem funkcjonalnym”. Jest zbudowany na Maszyna wirtualna BEAM firmy Erlang, system zaprojektowany w latach 80. XX wieku przez firmę Ericsson do zarządzania milionami jednoczesnych połączeń telefonicznych z czasem sprawności wynoszącym 99,9999999%. Elixir wprowadza na tej podstawie nowoczesną składnię, łańcuch narzędzi Mix/Hex i Phoenix Framework — jeden z najlepiej działających frameworków internetowych udokumentowane w branży.

Paradygmat funkcjonalny Elixiru nie jest wyborem estetycznym: nim jest co umożliwia odporność na błędy i masową współbieżność firmy BEAM. Aby zrozumieć OTP i GenServer (dalsze artykuły), musisz najpierw opanować te podstawy.

Czego się nauczysz

  • Instalacja i konfiguracja za pomocą Mix, IEx (interaktywna REPL)
  • Podstawowe typy: atom, krotka, lista, mapa, lista słów kluczowych
  • Dopasowywanie wzorców: najpotężniejsza funkcja Elixiru
  • Operator rury |>: Czytelny układ funkcji
  • Niezmienność: ponieważ nie można zmienić zmiennej
  • Funkcje: funkcje nazwane, anonimowe i wyższego rzędu
  • Moduły: Jak zorganizować kod w Elixir

Instalacja i konfiguracja

# 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

Typy podstawowe i struktury danych

# 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]

Dopasowywanie wzorców: rdzeń eliksiru

w eliksirze, = to nie jest zadanie: to operacja z mecz. Lewa strona jest „dopasowana” do prawej. Jeśli dopasowanie się powiedzie, zmienne we wzorcu zostaną powiązane z odpowiednimi wartościami. Jeśli się nie powiedzie, zostanie podniesione a 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

Operator rur: Skład transformacji

Operator rur |> przekazuje wynik poprzedniego wyrażenia jako pierwszy argument następnej funkcji. Przekształć zagnieżdżone wywołania w czytelnej sekwencji liniowej — kod wyraża potok transformacji w kolejności, w jakiej występują.

# 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)"},
# ]

Niezmienność: dane, które nigdy się nie zmieniają

W Elixirze struktury danych są niezmienne: nie można modyfikować listy, istniejącą mapę lub krotkę. Funkcje zawsze zwracają nowe struktury. Eliminuje to całą klasę błędów (skutki uboczne, współdzielony stan zmienny) i to właśnie sprawia, że konkurencja BEAM jest tak silna.

# 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

Moduły i funkcje

# 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

Wyliczenie i strumień: przetwarzanie kolekcji

# 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()

Wnioski

Paradygmat funkcjonalny Elixiru nie jest ograniczeniem: nim jest fundament, dzięki któremu wszystko inne jest możliwe. Dopasowanie wzorca eliminuje zagnieżdżone gałęzie warunkowe. Operator potoku renderuje transformacje danych czytelnych jak proza. Niezmienność zapewnia, że funkcje nie mają ukrytych skutków ubocznych. Zasady te, w połączeniu z BEAM VM, to właśnie one sprawiają, że Elixir jest tak odpowiedni dla systemów rozproszonych wysoka dostępność.

Nadchodzące artykuły z serii Elixir

  • Artykuł 2: BEAM i procesy — ogromna współbieżność bez współdzielonych wątków
  • Artykuł 3: OTP i GenServer — stan rozproszony z wbudowaną odpornością na błędy
  • Artykuł 4: Drzewa nadzorców — projektowanie systemów, które nigdy nie umierają