mercoledì 8 gennaio 2014

Introduzione alle funzioni lambda in C#

Introduzione alle funzioni lambda (lambda expressions)

Nei linguaggi di programmazione ad alto livello vengono spesso introdotte particolari tipi di funzioni, dette funzioni anonime o funzioni lambda. Rimanendo su un discorso relativamente semplice, le lambda trovano il loro utilizzo come valore di argomento passato ad altre funzioni. Per loro stessa natura sono una sorta di funzioni annidate, ovvero funzioni dichiarate all'interno di altre funzioni, di cui condividono la visibilità locale delle variabili. Questo le rende particolarmente adatte a query o alberi di espressioni per funzioni di livello superiore. Tratteremo gli alberi di espressioni in un post dedicato. A grandi linee sono una rappresentazione a grafo di una serie di istruzioni imperative da eseguire, che possono essere popolati e compilati a runtime.
Utilizzando una variabile dove il puntatore alla funzione viene memorizzato, possono essere rese ricorsive.

Personalmente trovo le lambda una componente fondamentale della programmazione moderna. Permettono di scrivere codice molto compatto, sopratutto quando sono necessari degli handlers che verranno utilizzati sporadicamente dove il codice da scrivere risulta particolarmente semplice, e quindi di rendere più leggibile il nostro codice e a volte di generare IL maggiormente ottimizzato. Le lambda sono anche indispensabili per lo sviluppo utilizzando query LINQ, funzionalità diventata indispensabile nella manipolazione di strutture dati complesse.

Sintassi in C#


Per creare una espressione lambda, viene utilizzato l'operatore => del C#, chamato "operatore lambda".
Viene utilizzato per separare le variabili di input della funzione, a sinistra, dal corpo funzione, a destra.

o più semplicemente:
(argomenti) => (corpo funzione)

La sintassi C# per le funzioni con un solo argomento prevede anche l'assenza delle parentesi. Mentre le funzioni con argomenti void richiedono delle parentesi vuote.

Esempio:
x => x * 2
oppure
() => { int a = 5; return a * 2; // restituisce 10}

Banalmente quindi un esempio funzionante che calcola il doppio di un valore x è il seguente:

class Program
{
    delegate int DoubleFunction(int value);
    static void Main(string[] args)
    {
        DoubleFunction calc = x => x * 2;
        int a = 0;

        a = calc(20);
        Console.WriteLine(a);
    }
}

Abbiamo prima definito il delegate di una funzione DoubleFunction. Dopodichè il corpo funzione è stato dichiarato usando una lambda e assegnato alla variabile calc di tipo DoubleFunction. Tramite il metodo Invoke di calc (o la forma compatta usata nell'esempio) possiamo utilizzare la funzione x * 2 e calcolare il doppio del parametro passato a Invoke. Quindi "calc(20)" restituirà 40.

Un esempio un poco più complesso invece risulta, ad esempio, la selezione delle parole che finiscono per "na" all'interno di una array di valori stringa:

static void SelectCollectionTutoral()
{
    String[] textArray = null;
    IEnumerable<string> selectResult = null;

    textArray = new String[]
    {
        "Cesena",
        "Ancora",
        "Pizza",
        "Porto",
        "Lanterna",
        "Rosso",
        "Ingrosso",
        "Ingranaggio",
        "Mediana"
    };

    selectResult = textArray.Where(el => el.EndsWith("na") == true);
    foreach (String s in selectResult)
        Console.WriteLine(s);
}

La lambda:

el => el.EndsWith("na") == true

contiene la logica della condizione Where, e si legge "all'elemento 'el' che mi passi, di tipo Stringa, applica la funzione EndsWith con parametro 'na'. Se il risultato della funzione è uguale a true, allora il risultato della lambda è uguale a true".
Quando la funzione lambda restituisce true infatti, la funzione Where seleziona l'elemento e lo inserisce dell'oggetto enumerabile.
Il tipo di dati degli argomenti e del valore restituito dalla lambda sono sempre impliciti e vengono ricavati direttamente dal compilatore.
La funzione Where fa parte delle estensioni del linguaggio fornite dal LINQ.

Visibilità delle variabili


Le varibili locali della funzione in cui è dichiarata una lambda sono visibili alla lambda stessa. Questo permette di scrivere del codice estremamente compatto e funzionale. Ad esempio, possiamo scrivere del codice asincrono che utilizzi delle funzioni locali senza fare uso di tecniche particolari per la condivisione della memoria:

static void StartTask()
{
    Task task = null;
    String baseText = "Element number {0} value: ";

    task = new Task(() =>
            {
                Random rnd = null;
                int count = 30;
                int maxValue = 10000;
                List<int> elements = null;

                rnd = new Random();
                elements = new List<int>();

                for (int i = 0; i < count; i++)
                    elements.Add(rnd.Next(0, maxValue));

                for (int i = 0; i < count; i++)
                    Console.WriteLine(String.Format(baseText, 
                                                    i + 1,
                                                    elements[i]));
            });
    task.Start();
    task.Wait();
}

Questo codice risulta sicuro a livello di memory leak, compatto e semplice da usare. Il corpo del thread è definito come una lambda (corrispondente al tipo di delegate base Action con prototipo void(void)) e viene mandato in esecuzione dopo averlo dichiarato. Ovviamente ho messo un Wait in modo da poter vedere scorrere l'esempio, altrimenti la funzione StarTask sarebbe uscita e il thread avrebbe continuato a funzionare indipendentemente.

Lo stesso codice senza lambda poteva essere scritto così:

static void TaskNoLambda()
{
    Task task = null;
    String baseText = "Element number {0} value: ";

    task = new Task(TaskBody, baseText);
    task.Start();
    task.Wait();
}

static void TaskBody(object state)
{
    Random rnd = null;
    int count = 30;
    int maxValue = 10000;
    List<int> elements = null;
    String baseText = null;

    baseText = state as String;

    rnd = new Random();
    elements = new List<int>();

    for (int i = 0; i < count; i++)
        elements.Add(rnd.Next(0, maxValue));

    for (int i = 0; i < count; i++)
        Console.WriteLine(String.Format(baseText,
                                        i + 1,
                                        elements[i]));
}

Viene da se che è risulta relativamente più complicato per l'uso che ne stiamo facendo, avendo una quantità maggiore di righe di codice e una gestione "spezzata" della funzione. L'oggetto di stato, ad esempio, in questo caso è semplice visto che è rappresentato da una stringa di testo, ma nel caso fosse necessario passare una certa quantità di dati, peggio se complessi, ci saremmo trovati in condizione di scrivere una classe dedicata al passaggio degli argomenti. In più, è premura del codice del thread essere sicuro che lo stato sia del tipo giusto ed eventualmente comportarsi di conseguenza.

Conclusioni


Le funzioni anonime sono ormai parte integrante di tutti i linguaggi a oggetti e non moderni. Sono utilizzate in moltissimi scenari, permettono di scrivere codice compatto e sono un aiuto in situazioni particolari dove è necessario avere una visibilità delle variabili e una flessibilità del codice non usuale.

Vorrei tentate a breve di verificare l'IL generato da una classe facente uso di lambda rispetto a una porzione di codice dove non se ne fa uso e si preferisce un approccio strutturale più classico per verificare l'impatto reale sul codice compilato. Preferisco dividere questo articolo rispetto a quello contenente intermediate language perché potrebbe rendere la lettura ancora più pesante, in maniera inopportuna visto già la difficoltà delle lambda sopratutto per chi viene da linguaggi dove le funzioni anonime non vengono usate o non si vedono spesso.

1 commento:

  1. Ottimo articolo.
    Spiegato in modo semplice e chiaro.

    Grazie.
    Marco

    RispondiElimina