Articoli di programmazione

Programmazione funzionale in C# - seconda parte

C#: Currying ed applicazione parziale delle funzioni

circuiti.jpg
Il Currying è una tecnica utilizzata nei linguaggi di programmazione funzionali per trasformare una funzione con più argomenti in una sequenza di funzioni, ciascuna delle quali prende solo un argomento. In C#, è possibile implementare il currying utilizzando le espressioni lambda.

Ecco un esempio di come implementare il currying in C#:


[TestMethod]
public void CurryingTest1()
{
Func< int, Func< int, int > > add = x => y => x + y;
var addTwo = add(2);

var result = addTwo(3);
Assert.AreEqual(5, result);

var result2 = add(2)(3);
Assert.AreEqual(5, result2);
}


In questo esempio, definiamo un delegato `Func>` chiamato `add`, che prende un argomento intero `x` e restituisce una nuova funzione che prende un argomento intero `y` e restituisce la somma di `x` e `y`. Utilizziamo le espressioni lambda per definire il delegato e la funzione restituita.

Utilizziamo poi l'applicazione parziale per creare una nuova funzione chiamata `addTwo` passando il valore intero `2` alla funzione `add`. La funzione `addTwo` prende un singolo argomento intero `y` e restituisce la somma di `2` e `y`. Infine, chiamiamo `addTwo` con il valore intero `3` e stampiamo il risultato.

Successivamente utilizziamo un differente tipo di chiamata alla funzione `add`.

Da notare che la chiamata è a cascata `add(2)(3)`: abbiamo di fatto scomposto la chiamata ad una funzione con più argomenti, in più chiamate a funzioni con argomento singolo.

A cosa serve? L'utilizzo del currying può rendere il codice più modulare e più facile da gestire. Vediamo un altro esempio più articolato: creiamo una funzione che calcola il prezzo totale di un prodotto in base al prezzo unitario e alla quantità, applicando uno sconto se il prezzo totale supera una certa soglia.

Ecco il codice:


[TestMethod]
public void CurryingTest2()
{
Func< decimal, Func < int, decimal > > calculateTotal = CalculateTotal();
decimal price = 10.0m;
int quantity = 5;
decimal total = calculateTotal(price)(quantity);
Console.WriteLine($"Price: {price:C}, Quantity: {quantity}, Total: {total:C}");

decimal discountThreshold = 40.0m;
Func< decimal, Func < int, decimal > > calculateDiscountedTotal = CalculateDiscountedTotal(discountThreshold);
price = 10.0m;
quantity = 5;
decimal discountedTotal = calculateDiscountedTotal(price)(quantity);
Console.WriteLine($"Price: {price:C}, Quantity: {quantity}, Discounted Total: {discountedTotal:C}");
}

static Func< decimal, Func< int, decimal > > CalculateTotal()
{
return price => quantity => price * quantity;
}

static Func< decimal, Func< int, decimal > > CalculateDiscountedTotal(decimal discountThreshold)
{
return price => quantity => {
decimal total = CalculateTotal()(price)(quantity);
if (total > discountThreshold)
{
total *= 0.9m; // 10% discount
}
return total;
};
}


In questo esempio, abbiamo definito due funzioni `CalculateTotal` e `CalculateDiscountedTotal` che restituiscono funzioni curried per il calcolo del prezzo totale del prodotto.

La funzione `CalculateTotal` prende il prezzo unitario decimal come primo argomento e la quantità int come secondo argomento, e restituisce il prezzo totale del prodotto. Usiamo la sintassi delle lambda expression per definire la funzione curried. Nota bene il tipo restituito da questa funzione perché in output abbiamo una Func che prende un decimal (price) e restituisce una funzione che a sua volta prende un int (quantity) e restituisce il risultato finale decimal (price* quantity).
Osservando questo costrutto si comprende come sia possibile eseguire l'applicazione parziale delle funzioni. Chiamando CalculateTotal solo col primo parametro Price, si ottiene in output una funzione che chiede Quantity per restituire il calcolo finale.

La funzione `CalculateDiscountedTotal` prende come primo argomento il valore soglia per lo sconto, e restituisce una funzione curried che prende il prezzo unitario come primo argomento e la quantità come secondo argomento. La funzione restituita calcola il prezzo totale del prodotto, applicando uno sconto del 10% se il prezzo totale supera la soglia specificata.

Creiamo così due funzioni curried: `calculateTotal` e `calculateDiscountedTotal`, tra l'altro una chiama l'altra al suo interno. Passiamo il prezzo e la quantità come argomenti parziali alle due funzioni per ottenere il prezzo totale del prodotto e il prezzo totale scontato, rispettivamente.

Utilizzando il currying in questo esempio, il codice risulta modulare e riutilizzabile, in quanto le funzioni sono scomposte in funzioni più semplici e possono essere combinate in modo flessibile per ottenere risultati diversi. Possono essere addirittura chiamate in modo differito, e questa è l'applicazione parziale tipica del Functional Programming , cioè dato che il risultato della funzione `calculateDiscountedTotal` è dato da 3 chiamate con singolo parametro, possiamo effettuare le chiamate quando ci torna meglio, invece di essere costretti a fare una singola chiamata quando abbiamo a disposizione tutti e 3 i parametri. Possiamo ad esempio fare una chiamata passando un parametro e salvarne il risultato in una variabile. Questa variabile è una funzione che si aspetta gli altri parametri. Possiamo passarla tra più livelli nella nostra applicazione e chiamarla ripetutamente in parti diverse.

Tutto questo è molto flessibile ma ricordiamoci sempre i cardini della logica funzionale:
- evitare effetti collaterali; ogni chiamata alle funzioni con un certo input deve ottenere sempre lo stesso output
- evitare di modificare gli oggetti che vengono dati in input, eventualmente restituire una nuova istanza in output; trattare gli oggetti come immutabili, per questo è molto comodo usare Record invece delle Classi
- evitare di scrivere queste funzioni in oggetti che usano uno stato e tantomeno farne uso; STATELESS, questa è la via!

#csharp #programmazione #codice #net #netcore #funzionale