1. Introduzione
Funzione pura — è una funzione che con gli stessi input restituisce sempre lo stesso risultato e non provoca effetti collaterali. In parole semplici: una funzione pura non "rompe" nulla intorno a sé e non è influenzata dall'esterno.
Una funzione è considerata pura se:
- Essa solamente calcola un valore basandosi sugli argomenti di input.
- Non modifica variabili esterne o lo stato del programma.
- Non dipende da variabili esterne o stati che possono cambiare.
Due regole d'oro della purezza
- Determinismo: lo stesso input — lo stesso output.
- Nessun effetto collaterale: la funzione non cambia nulla fuori di sé: né file, né variabili globali, né l'interfaccia utente (ciao, Console.WriteLine).
Non è religione, è buon senso
Potrebbe sembrare un'ossessione accademica, ma la pratica dimostra il contrario:
- Una funzione pura è prevedibile. È facile da testare — la invochi con argomenti definiti e ottieni un risultato definito.
- Una funzione pura può essere liberamente "rimescolata" nel codice senza temere che qualcosa si rompa altrove.
- In un mondo multicore una funzione pura è una garanzia di sicurezza: può essere eseguita in parallelo senza paura di race condition.
2. Esempi di funzioni pure e impure
Funzione pura: tutto prevedibile
// Funzione pura: non tocca nulla fuori di sé
int Add(int a, int b)
{
return a + b;
}
int Square(int x)
{
return x * x;
}
Chiamare Add(2, 3) anche cento volte di seguito — restituirà sempre 5. Noioso, ma affidabile.
Funzione impura: infrange le regole
// Infrange la purezza: dipende dallo stato esterno (variabile statica)
int counter = 0;
int Increase()
{
counter++;
return counter;
}
Qui la chiamata Increase() restituirà ogni volta un valore diverso — quindi non c'è più determinismo.
// Infrange la purezza: provoca un effetto esterno (stampa a schermo)
int AddAndPrint(int a, int b)
{
int sum = a + b;
Console.WriteLine(sum); // Effetto collaterale!
return sum;
}
E la casualità e il tempo?
Qualsiasi funzione che usa DateTime.Now o Random non è più pura:
// Non pura!
int GetRandomNumber()
{
return new Random().Next();
}
Tabella delle differenze
| Caratteristica | Funzione pura | Funzione impura |
|---|---|---|
| Sempre lo stesso risultato per gli stessi argomenti | Sì | No |
| Effetti collaterali | No | Sì |
| Dipendenza da stato esterno | No | Sì |
3. Immutabilità dei dati: teoria e pratica
Immutabilità (immutability) — è l'approccio in cui un oggetto non può essere modificato dopo la creazione. Se serve un nuovo valore — si crea un nuovo oggetto.
Perché è importante?
- L'applicazione diventa resistente a modifiche accidentali dei dati.
- Non ci sono "fughe" nascoste di modifiche: se hai un oggetto, nessuno lo cambierà di nascosto.
- L'immutabilità è la base per molte ottimizzazioni automatiche e per il calcolo parallelo.
Esempi semplici in C#
Tipi immutabili in .NET
Le stringhe (string) in C# sono immutabili! Ogni volta che fai string.Concat(s, "world"), viene creata una nuova stringa.
string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"
Array e collezioni: di default mutable
int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // L'array è cambiato!
Immutabilità "su due piedi": come compili il codice
Invece di modificare un oggetto/valore esistente, restituiamo uno nuovo:
// Invece di questo:
void AddToList(List<int> list, int value)
{
list.Add(value); // Mutazione!
}
// Meglio così:
List<int> AddToList(List<int> list, int value)
{
var newList = new List<int>(list) { value }; // Nuova lista
return newList;
}
Illustrazione: muta o non muta
flowchart LR
A[Oggetto originale] --"mutazione"--> B[Stesso oggetto, ma interno diverso]
A --"immutabilità"--> C[Nuovo oggetto]
4. Perché serve nel codice C# reale?
- Nel C# moderno librerie come LINQ, Entity Framework e ASP.NET Core puntano su funzioni pure e immutabilità.
- L'immutabilità riduce i bug "magici" dove qualcuno ha sovrascritto un valore importante da qualche parte.
- Le funzioni pure permettono di usare agevolmente i test automatici (unit test), perché per testare basta controllare input e output, senza preoccuparsi del mondo esterno.
Esempio: lavoro con le stringhe
string s = "Hello";
string newS = s.Replace("H", "J"); // s rimane "Hello"; newS è "Jello"
Esempio: LINQ e collezioni
Where, Select e altri metodi restituiscono nuove collezioni senza toccare quelle vecchie.
var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// numbers è rimasto uguale!
Esempio pratico: configurazione con oggetti immutabili
Molti API moderni di .NET usano oggetti immutabili per la configurazione, per esempio JsonSerializerOptions:
var options = new JsonSerializerOptions
{
WriteIndented = true
};
// Questo oggetto non viene cambiato "a runtime", aumentando l'affidabilità.
5. Errori tipici e trappole
La scivolata inizia quando "per sbaglio" muti dati in codice che dovrebbe essere puro.
Succede spesso con le collezioni: volevi filtrare una lista e nel processo hai cambiato quella originale.
O hai dimenticato che un metodo come List<T>.Add modifica l'oggetto sul posto.
Esempio insidioso:
List<int> DoubleTheNumbers(List<int> xs)
{
// Errore! Mutiamo la lista originale, restituendo lo stesso oggetto.
foreach (var i in xs)
xs.Add(i * 2);
return xs;
}
Questo codice provocherà anche un'eccezione di runtime (InvalidOperationException), perché stiamo modificando la collezione mentre la stiamo iterando — problema classico di mutazione.
Corretto:
List<int> DoubleTheNumbers(List<int> xs)
{
var newList = new List<int>(xs.Select(x => x * 2));
return newList;
}
GO TO FULL VERSION