CodeGym /Corsi /C# SELF /Applicazione pratica di FP in C#

Applicazione pratica di FP in C#

C# SELF
Livello 51 , Lezione 4
Disponibile

1. Introduzione

Passando dalla teoria alla pratica, è naturale chiedersi: «Perché a me, sviluppatore .NET e C#, servono tutte queste tecniche di programmazione funzionale?»
È vero, C# non è un linguaggio puramente funzionale come F# o Haskell. Ma a partire dalla versione 3.0 e fino a C# 14, ha acquisito molti strumenti FP che possono migliorare notevolmente la qualità e l'espressività del codice.

Ecco dove funzionano particolarmente bene:

  • Lavoro con le collezioniLINQ, Map/Reduce, filtraggio, aggregazione, ordinamento e altra "magia" sui dati.
  • Pure functions — meno bug dovuti allo stato e agli effetti collaterali, più semplice il debug.
  • Higher-order functions — componenti generiche e riutilizzabili con cui è piacevole lavorare.
  • Immutability in multithreading — uno dei principali presupposti per codice sicuro in scenari paralleli e asincroni.
  • Function composition — rende la logica di business complessa concisa, leggibile e facile da testare.

Tabella: Confronto approccio OOP vs approccio FP in C#

Compito Imperativamente (OOP/vecchia scuola) Funzionalmente (FP)
Filtrare una lista
foreach + if + Add
.Where(predicate)
Trasformare una lista
foreach + calcoli + Add
.Select(lambda)
Cercare con criterio
foreach + if/return
.FirstOrDefault(predicate)
Aggregazione
ciclo con variabile-contatore
.Aggregate(seed, func)
Cache
dizionario manuale + controlli
funzione con closure

2. LINQ: la cosa più funzionale in C#

Se ti è sembrato che “programmazione funzionale” significhi lavorare con liste, filtri e vari .Where, .Select, .Aggregate, — congratulazioni: è proprio così! LINQ è la quintessenza della FP in C#.

Ripassiamo come funziona LINQ

LINQ opera sulle collezioni tramite catene di metodi che accettano funzioni come parametri (per esempio lambda). Per esempio:

var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Prendiamo solo i numeri pari e raddoppiamoli
var result = numbers
    .Where(x => x % 2 == 0)
    .Select(x => x * 2);

foreach (var number in result)
    Console.WriteLine(number);

Cosa succede qui?

  • .Where — higher-order function: prende una funzione (x => x % 2 == 0) e restituisce un'altra collezione.
  • .Select — prende anch'essa una funzione (x => x * 2).
  • Non modifichiamo la collezione originale, otteniamo un nuovo risultato.

Questo stile è facile da leggere ed estendere (puoi appendere altri .OrderBy, .Take, .Distinct, ecc.).

Nota! Le espressioni lambda sono un modo comodo per creare un "delegate al volo". LINQ sarebbe impossibile senza il supporto alla FP in C#.

Schema: Elaborazione funzionale di una collezione


Collezione --> Where(x => bool) --> Select(x => y) --> Nuovo risultato

3. Composizione di funzioni e pipeline di elaborazione dati

La FP usa spesso la composizione: un'operazione complessa è costruita come catena di piccole funzioni, ognuna fa il suo lavoro.

Esempio: pipeline di trasformazione di una stringa

Stile mutabile (OOP):

string s = "   hello world   ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!

Più "funzionale" — come pipeline di funzioni:

Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";

// Composizione di funzioni — le applichiamo in sequenza
Func<string, string> pipeline = x => addBang(upper(trim(x)));

Console.WriteLine(pipeline("   hello world   ")); // HELLO WORLD!

Semplice combinatore di composizione:

Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
    x => g(f(x));

// E ora pipeline tramite Compose:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);

Console.WriteLine(pipeline2("   hello again    ")); // HELLO AGAIN!

4. Lavorare con l'immutabilità: protezione dai bug

L'immutability è il mattone fondamentale della FP. Non modifichiamo le strutture dati, ma ne ritorniamo di nuove. Questo è particolarmente importante nelle applicazioni multithread.

Esempio: "sbagliato" (mutabile)

List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;

Esempio: "corretto" (funzionale)

var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();

Nel moderno C# ci sono collezioni ImmutableList<T> e altri tipi nello spazio dei nomi System.Collections.Immutable:

using System.Collections.Immutable;

var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // Ritorna una nuova lista!

5. Higher-order functions nella vita reale

Le higher-order functions sono un modo per scrivere componenti generiche senza tanti rami condizionali.

Esempio: filtro generico per utenti

class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var users = new List<User>
{
    new User { Name = "Vasya", Age = 26 },
    new User { Name = "Katya", Age = 17 },
    new User { Name = "Lyosha", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
    return source.Where(u => predicate(u)).ToList();
}

// Uso:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);

6. Pattern matching e switch-expressions

Il moderno C# usa molto il pattern matching: le switch-expressions spesso rimpiazzano lunghe catene di if.

object value = 123;

string description = value switch
{
    int i when i > 100 => "Numero grande",
    string s when s.Length > 3 => "Stringa lunga",
    null => "Valore vuoto",
    _ => "Sconosciuto"
};

Console.WriteLine(description); // Numero grande

7. Memoization: caching dei risultati delle funzioni

La memoization è il caching del risultato di una funzione per argomenti uguali. In C# è facile implementarla da soli.

Func<int, int> SlowFib = null; // Funzione ricorsiva Fibonacci

var cache = new Dictionary<int, int>();

SlowFib = n =>
{
    if (cache.ContainsKey(n))
        return cache[n];
    if (n <= 1)
        cache[n] = n;
    else
        cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
    return cache[n];
};

Console.WriteLine(SlowFib(40)); // Fulmineo!

8. Currying e partial application

La partial application è il fissare parte degli argomenti di una funzione. In C# è comodo farla con le lambda.

Func<int, int, int> add = (a, b) => a + b;

// Fissiamo il primo argomento
Func<int, int> add10 = b => add(10, b);

Console.WriteLine(add10(5));   // 15
Console.WriteLine(add10(100)); // 110

9. Stile dichiarativo con le funzioni

Imperativamente:

var result = new List<int>();
foreach (var n in numbers)
{
    if (n > 0)
        result.Add(n * n);
}

Dichiarativamente:

var result = numbers
    .Where(n => n > 0)
    .Select(n => n * n)
    .ToList();

10. Esercizio pratico

Implementiamo un modulo di filtraggio delle task in un "task manager per studenti".

Modello:

class StudentTask
{
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
    public int Priority { get; set; }
}

Dati iniziali:

var tasks = new List<StudentTask>
{
    new StudentTask { Title = "Fare i compiti", IsCompleted = false, Priority = 2 },
    new StudentTask { Title = "Bere un caffè", IsCompleted = true, Priority = 3 },
    new StudentTask { Title = "Guardare la lezione", IsCompleted = false, Priority = 1 }
};

Filtro generico:

List<StudentTask> FilterTasks(
    List<StudentTask> all,
    Predicate<StudentTask> predicate)
{
    return all.Where(t => predicate(t)).ToList();
}

// Cerca task non completate con priority > 1
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);

// Stampa risultato
foreach (var task in importantTasks)
    Console.WriteLine(task.Title);

Combinatori di predicati:

Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;

// Uniamo più criteri, variante 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));

// Variante 2: funzione-combinatore di due predicati
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);

var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));

11. Peculiarità e errori tipici nell'adottare lo stile funzionale in C#

Prima di tutto, ricorda che C# è un linguaggio fortemente tipizzato. A volte è necessario specificare i tipi esplicitamente, specialmente quando le funzioni ritornano delegate o lambda complesse. Altrimenti puoi ricevere errori di compilazione per tipi non dedotti.

Secondo, non passare funzioni con effetti collaterali dove si aspettano pure functions. Modificare variabili esterne rompe la prevedibilità. Cerca di avere funzioni che non mutano lo stato esterno al loro scope.

Terzo, fai attenzione al capture delle variabili esterne (closure capture), soprattutto in codice asincrono e multithread. Una variabile il cui valore cambia dopo essere stata catturata in una lambda (per esempio in LINQ) può portare a bug non ovvi.

Infine, un uso eccessivo dello stile FP può complicare la manutenzione per un team non abituato. Usa la FP dove semplifica davvero la soluzione, non solo per "bellezza".

1
Sondaggio/quiz
Programmazione funzionale, livello 51, lezione 4
Non disponibile
Programmazione funzionale
Introduzione alla programmazione funzionale
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION