Elixir from Scratch: Funkční paradigma, Pattern Matching a Pipe Operator
Úvod do Elixir pro vývojáře s OOP pozadím: jako porovnávání vzorů nahrazuje klasický if/switch, potrubní operátor |> pro elegantní kompozici, neměnnost dat a proč to vede k předvídatelnějšímu kódu.
Proč Elixír v roce 2026
Elixir není „jen další funkční jazyk“. Je postaven na Erlangův BEAM Virtual Machine, systém navržený v 80. letech 20. století společností Ericsson spravovat miliony souběžných telefonních spojení s 99,9999999% dostupností. Elixir přináší na tomto základě moderní syntaxi, Mix/Hex toolchain a Phoenix Framework — jeden z nejvýkonnějších webových frameworků zdokumentováno v oboru.
Funkční paradigma Elixíru není estetickou volbou: je co umožňuje odolnost proti chybám a masivní souběžnost BEAM. Chcete-li porozumět OTP a GenServeru (pozdější články), musíte nejprve zvládnout tyto základy.
Co se naučíte
- Instalace a nastavení pomocí Mix, IEx (interaktivní REPL)
- Základní typy: atom, n-tice, seznam, mapa, seznam klíčových slov
- Shoda vzorů: Nejvýkonnější funkce Elixíru
- Operátor potrubí |>: Čitelná kompozice funkcí
- Neměnnost: Protože nemůžete změnit proměnnou
- Funkce: pojmenované, anonymní funkce a funkce vyššího řádu
- Moduly: Jak organizovat kód v Elixir
Instalace a nastavení
# 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
Základní typy a datové struktury
# 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]
Pattern Matching: The Elixir Core
V Elixíru, = to není úkol: je to operace
z zápas. Levá strana je „srovnaná“ s pravou stranou.
Pokud je shoda úspěšná, proměnné ve vzoru jsou svázány s odpovídajícími hodnotami.
Pokud selže, je zvýšeno 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
Pipe Operator: Složení transformací
Provozovatel potrubí |> předá výsledek předchozího výrazu
jako první argument další funkce. Transformujte vnořené hovory
v čitelné lineární sekvenci — kód vyjadřuje zřetězení
transformace v pořadí, v jakém se vyskytují.
# 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)"},
# ]
Neměnnost: Data, která se nikdy nemění
V Elixir jsou datové struktury neměnné: nemůžete upravit seznam, existující mapu nebo n-tice. Funkce vždy vracejí nové struktury. To eliminuje celou třídu chyb (vedlejší účinky, sdílený proměnlivý stav) a to je to, co dělá konkurenci BEAM tak robustní.
# 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
Moduly a funkce
# 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 a Stream: Zpracování kolekce
# 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()
Závěry
Funkční paradigma Elixíru není omezení: je základ, který umožňuje všechno ostatní. Odstranění shody vzoru vnořené podmíněné větve. Operátor potrubí vykreslí transformace dat čitelných jako próza. Neměnnost zajišťuje, že funkce nemají skryté vedlejší účinky. Tyto principy v kombinaci s BEAM VM, díky nim je Elixir tak vhodný pro distribuované systémy vysoká dostupnost.
Připravované články ze série Elixír
- Článek 2: BEAM a procesy — masivní souběžnost bez sdílených vláken
- Článek 3: OTP a GenServer — Distribuovaný stav s vestavěnou odolností proti chybám
- Článek 4: Stromy dohledu — Navrhování systémů, které nikdy neumírají







