Stream e iteratori
Sicuramente gli stream sono la prima caratteristica di Cω da analizzare poichè con essi trovano le basi molte altre feature messe a disposizione da questo linguaggio. Per dar loro una definizione potremmo parlare di una collezione omogenea e ordinata di zero o più elementi ma, a differenza degli array, di carattere lazy cioè costruiti solo quando necessario. Per dichiarare uno stream non occorre fare altro che postporre l'operatore * al tipo della variabile, quindi per fare un esempio la dichiarazione di uno stream di string sarà scritta nel seguente modo:

string* stringhe;

Ora che abbiamo definito il nostro stream di stringhe possiamo occuparci di come popolarlo e utilizzarlo, creando una funzione generatrice che chiameremo, in maniera abbastanza autoesplicativa, GetIterator e un'altra funzione per effettuare dei semplici test sul nostro stream:

public string* GetIterator()
{
    yield return "uno";
    yield return "due";
    yield return "tre";
}

public void Test()
{
    string
* stringhe = GetIterator();
    foreach (string stringa in stringhe)
      Console.WriteLine(stringa);
}

Possiamo notare una cosa interessante in questo stralcio di listato come la presenza della keyword yield return, già nota a chi ha avuto modo di utilizzare C# 2.0 nell'ambito degli iteratori e che in Cω ricopre il medesimo ruolo ovvero avvertire il compilatore di generare automaticamente il codice equivalente a una classe di enumerazione che implementa le interfacce IEnumerable e IEnumerator, permettendoci pertanto anche l'utilizzo di foreach per effettuare l'iterazione dello stream. Questo significa che internamente uno stream non si differenzia in maniera particolare da un iteratore di C# 2.0, anche se per ovviare alla mancanza dei generics in Cω (che come detto si basa sul vecchio CLR) il compilatore si occupa di creare delle interfacce ad hoc per tutti i tipi che nel nostro programma verranno chiamati in causa da eventuali enumerazioni, interfacce che verranno poi implementate dalle stesse classi generate dai nostri yield return. Tornando al nostro codice di esempio, come output risultante dalla sua esecuzione otterremo:

uno
due
tre

In realtà possiamo ottenere il medesimo output scrivendo il nosto esempio in un altro modo, questa volta sfruttando un costrutto denominato apply-to-all-expressions:

public void Test()
{
    
string* stringhe = GetIterator();
    stringhe.{  Console.WriteLine(it);  };
}

In questo esempio viene effettuata sempre un'iterazione ma allo stream viene allegato un blocco di codice sotto forma di funzione anonima applicata a ogni elemento durante l'iterazione stessa. Per rendere possibile l'identificazione dell'elemento corrente viene utilizzata una variabile speciale chiamata it, già correttamente tipizzata dal compilatore in accordo con il tipo base dello stream evitandoci quindi di dover dichiarare manualmente una variabile come nel caso di un ciclo foreach. La possibilità di richiamare una funzione per tutti gli elementi di uno stream non si limita solo ai metodi anonimi ma vale anche per proprietà o metodi definiti dal tipo di elementi contenuti nello stream. Questo significa che anche il seguente codice è valido e funzionante:

public void Test()
{
    
string* stringhe = GetIterator();
    
stringhe.ToUpper().{  Console.WriteLine(it);  };
}

In questo esempio per ogni elemento dello stream viene invocato prima il metodo ToUpper() definito per le istanze di tipo stringa e in seguito il risultato viene processato all'interno della funzione anonima che lo segue, per cui otterremo il seguente output:

UNO
DUE
TRE

Supponiamo però che a questo punto ci si presenti la necessità di dover ottenere come output solo uno o comunque solo alcuni degli elementi contenuti nello stream, quasi sicuramente ci verrebbe naturale l'idea di utilizzare degli IF o dei SELECT - CASE all'interno della funzione anonima:

public void Test()
{
    
string* stringhe = GetIterator();
    stringhe.ToUpper().{  
        
if (it == "DUE")
            Console.WriteLine(it);  
    };
}

Di sicuro questo approccio ci ha permesso di raggiungere il nostro scopo di filtrare l'output ottenendo in questo caso solamente la stringa "DUE", ma allo stesso tempo abbiamo introdotto del codice che, in quanto a stile, va in contrasto con la compattezza dello stesso che abbiamo potuto riscontrare fino adesso e la situazione peggiorerebbe nel caso dovessimo definire più di una casistica. Fortunatamente il team di Cω ha pensato anche a questo mettendo a disposizione dello sviluppatore i cosidetti filtri, un'altra nuova feature che permette di filtrare uno stream in base a uno o più criteri:

public void Test()
{
    
string* stringhe = GetIterator();
    stringhe.ToUpper()[it == "DUE"].{  Console.WriteLine(it);  };    

}

Questa ulteriore elaborazione del nostro esempio mette in evidenza come sia possibile applicare un filtro che ci permetta di ottenere solo alcuni elementi di uno stream in base a un criterio da noi specificato, nel nostro caso applicato a ogni valore restituito dal metodo ToStream() il quale a sua volta viene invocato per ogni elemento di tipo stringa contenuto dallo stream. Come è facile immaginare l'output generato della nostra funzione sarà:

DUE

Andando a sbirciare il codice prodotto dal compilatore scopriremmo che in realtà questi filtri vengono tradotti in semplici IF all'interno della funzione MoveNext() dello stream, risultando quindi in un approccio non molto differente dal nostro iniziale almeno a livello logico. Detto questo, nulla vieta allo sviluppatore di specificare più di una clausola come già anticipato e pertanto possiamo anche applicare un filtro di questo tipo:

public void Test()
{
    
string* stringhe = GetIterator();
    stringhe.ToUpper()[it == "DUE" || it.StartsWith("T")].{
        Console.WriteLine(it);
    };
}

Questo esempio si traduce nel seguente output:

DUE
TRE

Indubbiamente si tratta di un approccio molto comodo che risolve le nostre iterazioni con poche righe di codice, se non addirittura con una in casi semplici come quelli degli esempi di questo articolo. Tuttavia stream e filtri non sono nati con il solo scopo di semplificare il nostro codice ma il vero vantaggio è che la loro esistenza ha messo a disposizione degli sviluppatori la possibilità di eseguire vere e proprie interrogazioni sugli oggetti sotto forma di query XPath argomento che verrà approfondito in uno degli articoli successivi.

Nel prossimo articolo ci dedicheremo invece alle strutture anonime (definite anche tuple).

powered by IMHO 1.2