F# Primer
Utilizzo delle tecniche di programmazione funzionale in .NET Framework
Ted Neward
In questo articolo verranno discussi i seguenti argomenti:
- Installazione di F#
- Concetti di base del linguaggio F#
- Interoperabilità .NET
- Modalità asincrona in F#
|
In questo articolo verranno utilizzate le seguenti tecnologie:
.NET Framework, F#
|

Sommario
Di recente incluso nella famiglia di prodotti Microsoft® .NET Framework, F# garantisce indipendenza dai tipi, prestazioni elevate e la capacità di fungere da linguaggio di script, il tutto come parte integrante dell'ambiente .NET. Questo linguaggio funzionale è stato creato da Don Syme di Microsoft Research come variante OCaml compatibile con la sintassi per CLR, ma F# è stato rapidamente fatto uscire dal laboratorio per avviare una sperimentazione sul campo.
In conseguenza dell'introduzione dei concetti della programmazione funzionale nei linguaggi più diffusi come C# e Visual Basic® tramite tecnologie quali LINQ e generics .NET, F# ha guadagnato una sempre crescente visibilità all'interno della community .NET, raggiungendo una portata tale che nel mese di novembre 2007 Microsoft ha annunciato che F# entrerà ufficialmente nella schiera dei linguaggi di programmazione .NET supportati.
Per anni l'area dei linguaggi funzionali (ML, Haskell e così via) è stata considerata più appropriata per la ricerca accademica piuttosto che per lo sviluppo professionale. La ragione di ciò non risiede affatto nel poco interesse suscitato da questi linguaggi. In effetti, alcuni importanti miglioramenti introdotti in .NET, ad esempio generics, LINQ, PLINQ e Futures, derivano dall'applicazione dei concetti alla base della programmazione funzionale a linguaggi in cui tali concetti erano completamente sconosciuti. La mancanza di interesse per questi linguaggi è stata basata più sul fatto che si trattava di linguaggi destinati a piattaforme di scarsa rilevanza per gli sviluppatori di programmi per Windows®, che non garantivano una corretta integrazione con la piattaforma sottostante o che non supportavano funzionalità chiave come l'accesso a database relazionali, l'analisi XML e i meccanismi di comunicazione out-of-process.
Tuttavia, CLR e l'approccio su cui si basa, che prevede il supporto per più linguaggi basati su un'unica piattaforma, ha reso inevitabile l'introduzione di un numero sempre maggiore di questi linguaggi nel mondo dello sviluppo Windows. Analogamente, era inevitabile che tali linguaggi iniziassero a far sentire la loro presenza tra i programmatori professionali. F# è uno di questi linguaggi. In questo articolo verranno presentati alcuni dei concetti alla base di F# e verranno illustrati i vantaggi offerti da questo linguaggio. Successivamente, verranno illustrate le procedure di installazione e di scrittura di diversi programmi semplici, che risulteranno di particolare aiuto per iniziare ad apprendere l'utilizzo di F#.
Perché utilizzare F#?
Sarà ovvio a una piccola percentuale di programmatori .NET che l'apprendimento di un linguaggio funzionale per .NET Framework rappresenta un significativo passo in avanti nella scrittura di applicazioni software potenti. Per il resto, la motivazione per apprendere F# è un completo mistero. Quali sono i vantaggi offerti da F# agli sviluppatori?
La scrittura di programmi concorrenti protetti è diventata negli ultimi tre anni una questione essenziale, in particolare in conseguenza della crescente diffusione delle CPU multicore. I linguaggi funzionali consentono agli sviluppatori di supportare la concorrenza favorendo strutture di dati immutabili che possono essere scambiate tra thread e computer senza dover preoccuparsi della protezione dei thread o dell'accesso atomico. I linguaggi funzionali consentono inoltre di semplificare la scrittura di librerie più efficienti e compatibili con la concorrenza, come i flussi di lavoro asincroni F#, che verranno esaminati più avanti in questo articolo.
A differenza di quanto comunemente ritenuto dai programmatori focalizzati sullo sviluppo orientato a oggetti, i programmi funzionali risultano spesso più semplici da scrivere e gestire per determinati tipi di applicazioni. Si supponga, ad esempio, di scrivere un programma per convertire un documento XML in un formato di dati differente. Anche se sarebbe certamente possibile scrivere un programma C# che analizzasse il documento XML e applicasse diverse istruzioni if per determinare quali azioni intraprendere in diversi punti del documento, un approccio senza dubbio di superiore efficacia consiste nella scrittura della trasformazione come programma XSLT (eXtensible Stylesheet Language Transformation). Questo non sorprende affatto in quanto XSLT, analogamente a SQL, include al suo interno una forte dose di funzionalismo.
F# scoraggia l'utilizzo di valori null e promuove l'utilizzo di strutture di dati immutabili. Queste strutture consentono di limitare la frequenza di errori durante la programmazione riducendo la quantità di codice speciale necessario.
I programmi scritti in F# tendono inoltre a essere più concisi. L'utilizzo di questo linguaggio consente di limitare la digitazione, in entrambi i significati del termine: minor numero di pressioni di tasti e minor numero di posizioni in cui è necessario indicare al compilatore il tipo su cui impostare la variabile, gli argomenti o il tipo restituito. Questo si traduce in una significativa riduzione della quantità di codice da gestire.
F# offre un profilo di prestazioni simile a C#. Tuttavia, fornisce un profilo di prestazioni molto più efficace rispetto a linguaggi concisi simili, in particolare i linguaggi dinamici e di script. Analogamente alla maggior parte dei linguaggi dinamici, F# include gli strumenti che consentono di esplorare i dati scrivendo frammenti di programmi ed eseguendoli in modalità interattivo.
Installazione di F#
Disponibile come download gratuito all'indirizzo
research.microsoft.com/fsharp/fsharp.aspx, F# consente di installare non solo tutti gli strumenti della riga di comando ma anche un package di estensione di Visual Studio
® che fornisce l'evidenziazione della sintassi con colore, modelli di file e di progetto, incluso un esempio molto dettagliato di codice F# come guida iniziale) e il supporto per IntelliSense
®. È disponibile inoltre una shell F# interattiva, che può essere eseguita in Visual Studio, consentendo agli sviluppatori di copiare le espressioni dalle finestre dei file di origine, incollarle nella finestra della shell interattiva e visualizzare i risultati immediati dal frammento di codice, all'interno di una finestra dedicata migliorata.
Al momento della redazione di questo articolo, F# viene eseguito come strumento esterno all'interno di Visual Studio, il che implica che non offre lo stesso grado di continuità che gli sviluppatori ottengono con C# o Visual Basic. F# non offre inoltre il supporto per la finestra di progettazione delle pagine di ASP.NET. Questo tuttavia non significa che F# non possa essere utilizzato in ASP.NET, ma semplicemente che il supporto di Visual Studio per F# non fornisce per F# lo stesso tipo di funzionalità di sviluppo tramite trascinamento offerto per C# e Visual Basic.
Tuttavia, la versione corrente di F# può essere utilizzata in qualsiasi ambiente in cui è possibile utilizzare altri linguaggi compatibili con .NET. Nelle pagine successive verranno illustrati alcuni esempi.
Hello, F#
L'introduzione all'apprendimento di qualsiasi nuovo linguaggio è inevitabilmente accompagnata dall'onnipresente programma "Hello, World". F# non interrompe questa tradizione:
Sebbene un po' semplicistico, questo piccolo esempio mostra che F# appartiene a quella categoria di linguaggi che non richiede un punto di ingresso esplicito (a differenza di C#, Visual Basic e C++/CLI); il linguaggio presuppone che la prima riga del programma sia il punto di ingresso ed esegue il programma da tale riga.
Per eseguire questo programma, lo sviluppatore F# può scegliere tra due tipi di programmi: compilato o interpretato. L'esecuzione di tale programma all'interno dell'interprete F# (fsi.exe) è piuttosto semplice. Avviare semplicemente fsi.exe dalla riga di comando e inserire la riga sopra riportata nel prompt risultante, come illustrato nella Figura 1.
Figura 1 Esecuzione di 'Hello, World' all'interno dell'interprete F# (Fare clic sull'immagine per ingrandirla)
Tenere presente che nella shell l'istruzione deve essere terminata con due punti e virgola. Questa è una peculiarità della modalità interattiva e non è necessaria per i programmi F# compilati.
Per eseguire questo esempio come eseguibile .NET standard, attivare Visual Studio secondo la normale procedura e creare un nuovo progetto F# (disponibile in Altri tipi di progetto). Un progetto F# è composto, all'inizio, da un solo file di origine F#, denominato file1.fs. Se si apre questo file, verrà visualizzata una grande quantità di codice F# di esempio. Esaminarne il contenuto, semplicemente per avere un'idea della sintassi F#. Al termine, sostituire l'intero file con il codice "Hello, world!" riportato in precedenza, quindi eseguire l'applicazione. "Hello, world!" verrà visualizzato in una finestra dell'applicazione console.
Se si preferisce una riga di comando, è possibile compilare il codice utilizzando lo strumento fsc.exe disponibile nella sottodirectory \bin della directory di installazione F#. Notare che il file fsc.exe si comporta come la maggior parte dei compilatori della riga di comando, inserendo il codice sorgente nella riga di comando e producendo un eseguibile come risultato. La maggior parte delle opzioni della riga di comando è documentata, anche se molte di esse dovrebbero già essere note a chi dispone di una certa esperienza con il compilatore csc.exe o cl.exe. Tenere presente, tuttavia, che un'area in cui F# correntemente presenta delle lacune è rappresentata dal supporto per MSBuild; infatti, non viene fornito un supporto diretto all'interno dell'installazione corrente (1.9.3.7 al momento della redazione dell'articolo) per la compilazione basata su MSBuild.
Se si preferisce che il programma "Hello, world!" sia un po' più grafico, F# fornisce una fedeltà e un'interoperabilità complete con la piattaforma CLR sottostante, incluse le librerie Windows Forms. Provare il seguente esempio:
System.Windows.Forms.MessageBox.Show "Hello World"
La possibilità di utilizzare la libreria di classi di .NET Framework e le librerie F# rende F# un linguaggio allettante sia per la comunità matematica e scientifica, in cui già vengono utilizzati linguaggi funzionali come OCaml o Haskell, che per il corpo esistente di sviluppatori NET in tutto il mondo.
L'espressione let
A questo punto, si esamini parte del codice F#, che non è affatto semplice quanto il tradizionale programma "Hello, world!". Si consideri l'esempio seguente:
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
Un elemento curioso nella sintassi F# è rappresentato dall'espressione let. Questa è l'espressione più importante dell'intero linguaggio. Formalmente, let assegna un valore a un identificatore. Qualsiasi sviluppatore Visual Basic o C# sarebbe tentato di tradurre questa espressione in quanto "definisce una variabile". Questa supposizione sarebbe errata. Gli identificatori in F# incorporano due principi. Primo, un identificatore, una volta definito, non può mai essere modificato. In questo modo F# consente ai programmatori di creare programmi indipendenti dalla concorrenza in quanto scoraggia lo stato mutabile. Secondo, l'identificatore può essere non solo un tipo di primitiva o di oggetto, come in C# e Visual Basic, ma anche un tipo di funzione, analogamente a quanto avviene in LINQ.
Notare inoltre che gli identificatori non vengono mai definiti in modo esplicito come dotati di tipo. L'identificatore results, ad esempio, non è mai definito, ma viene dedotto dalla parte destra dell'espressine che lo segue. Questo comportamento viene definito inferenza del tipo e rappresenta la capacità del compilatore di analizzare il codice, determinare il valore restituito e aggiungerlo automaticamente (questa funzionalità è simile alle nuove espressioni dei tipi dedotti di C# tramite la parola chiave var).
Non è necessario utilizzare l'espressione let esclusivamente con i dati. È possibile utilizzarla per definire le funzioni, che F# riconosce come concetti di prima classe. Nel seguente esempio viene definita una funzione add che accetta due parametri, a e b:
L'implementazione fornisce i risultati previsti: aggiunta di a e b e restituzione implicita del risultato al chiamante. Ne consegue che, dal punto di vista tecnico, ogni funzione in F# restituisce un valore, anche se tale valore non è un vero e proprio valore, noto con il nome speciale unit. Questo avrà alcune implicazioni interessanti nel codice F#, in particolare laddove si interseca con la libreria di classi di .NET Framework ma, per ora, gli sviluppatori C# e Visual Basic possono considerare unit più o meno come l'equivalente di void.
Esistono alcune situazioni in cui sarebbe opportuno che una funzione ignorasse un parametro passato. Per ottenere questo risultato in F#, utilizzare semplicemente il carattere di sottolineatura come segnaposto per il parametro:
let return10 _ =
add 5 5
// 12 is effectively ignored, and ten is set to the resulting
// value of add 5 5
let ten = return10 12
printf "ten = %d\n" ten
Come molti linguaggi funzionali, F# consente il cosiddetto currying o applicazione parziale, in cui l'applicazione di una funzione può essere definita solo parzialmente, basandosi sulla relativa chiamata per fornire i parametri rimanenti:
Sotto certi aspetti, questa funzionalità è simile alla creazione di un metodo di overload che accetta un insieme differente di parametri e chiama un altro metodo:
public class Adders {
public static int add(int a, int b) { return a + b; }
public static int add5(int a) { return add(a, 5); }
}
Esiste tuttavia una sottile differenza. Notare che nella versione F# nessun tipo viene definito in modo esplicito. Ne consegue che il compilatore tenterà di dedurre il tipo, determinerà se il parametro di add5 è di tipo compatibile con l'aggiunta al valore letterale intero 5 ed eseguirà la compilazione di conseguenza o genererà un errore. Infatti, il linguaggio F# prevede la parametrizzazione di tipo implicita, ovvero fa uso di generics.
In Visual Studio, se si passa il puntatore sulla definizione riportata in precedenza di ten si scoprirà che il relativo tipo viene dichiarato nel seguente modo:
In F# questo significa che ten è un valore, una funzione che accetta un parametro di qualsiasi tipo e produce un risultato int. Poiché la sintassi tick equivale approssimativamente alla sintassi <T> in C#, facendo un paragone con una funzione C#, si potrebbe affermare che ten è simile a un'istanza di delegato per un metodo con parametri di tipo, il cui tipo sarebbe opportuno ignorare (anche se le regole di C# non lo consentono):
delegate int Transformer<T>(T ignored);
public class App
{
public static int return10(object ignored) { return 5 + 5; }
static void Main()
{
Transformer<object> ten = return10;
System.Console.WriteLine("ten = {0}", return10(0));
}
}
La parola chiave for
Ora si esamini la parola chiave for riportata nel primo esempio:
#light
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
Partendo dall'inizio del codice, notare la sintassi #light. Si tratta di una concessione ai programmatori non OCaml che si stanno avvicinando al linguaggio F#, allo scopo di rendere meno rigidi i requisiti del linguaggio OCaml a livello di sintassi e di consentire l'utilizzo di una notevole quantità di spazi per definire i blocchi di codice. Sebbene non sia necessario, l'opzione #light rende la sintassi più semplice da analizzare per lo sviluppatore C# o Visual Basic medio e pertanto viene utilizzata di frequente negli esempi relativi a F# e nei frammenti di codice pubblicati, diventando lo standard de facto per la programmazione F#. È possibile che in una versione futura di F# #light diventi la sintassi predefinita, non il contrario.
Il ciclo for apparentemente innocuo è, in realtà, tutt'altro che semplice. Ufficialmente, si tratta di un elenco generato, ovvero rappresenta un blocco di codice che produrrà un risultato che corrisponde a un elenco.
Un elenco è un costrutto primitivo utilizzato di frequente nei linguaggi funzionali e, sotto questo aspetto, è simile a una matrice. Tuttavia, un elenco non consente l'accesso basato sulla posizione, come la tradizionale sintassi a[i] di C#. Gli elenchi vengono visualizzati in diverse posizioni nella programmazione funzionale e, per la maggior parte, possono essere considerati come l'equivalente in F# di .NET Framework List<T> con alcune funzionalità migliorate.
Un elenco è sempre di tipo particolare e, in questo caso, l'identificatore results rappresenta un elenco di tuple, specificamente il tipo di tupla identificato in F# come tipo (int * int). Questa idea di un elenco di tuple è familiare se considerata come l'equivalente di una coppia di colonne restituite da un'istruzione SELECT in SQL. Pertanto, nell'esempio viene creato essenzialmente un elenco di coppie di numeri interi composto da 100 elementi.
Comunemente, nei linguaggi funzionali, le definizioni delle funzioni vengono utilizzate ovunque sia possibile utilizzare il codice stesso. Pertanto, se si desidera estendere l'esempio precedente, scrivere quanto riportato di seguito:
let compute2 x = (x, x*x)
let compute3 x = (x, x*x, x*x*x)
let results2 = [ for i in 0 .. 100 -> compute2 i ]
let results3 = [ for i in 0 .. 100 -> compute3 i ]
Questa idea di eseguire un ciclo in un elenco (o una matrice o qualche altro costrutto iterabile) è un'attività talmente comune nei linguaggi funzionali che è stata generalizzata come chiamata di metodo di base: List.iter. Questa funzione prevede semplicemente una chiamata a una funzione in ciascun elemento dell'elenco. Funzionalità molto utili sono inoltre fornite da altre funzioni di libreria simili. Ad esempio, List.map accetta una funzione come argomento e applica la funzione a ciascun elemento dell'elenco, restituendo un nuovo elenco nel processo.
La pipeline
A questo punto si esaminerà un altro costrutto di F#, l'operatore pipeline, che accetta i risultati di una funzione e, in modo simile ai pipe nelle shell dei comandi (come Windows PowerShell®) li utilizza come input per una funzione successiva. Considerare il frammento di codice F# illustrato nella Figura 2. In questo codice viene utilizzato lo spazio dei nomi System.Net per la connessione a un server HTTP, l'acquisizione dell'HTML corrispondente e l'analisi dei risultati.

Figure 2 Recupero e analisi di HTML
/// Get the contents of the URL via a web request
let http(url: string) =
let req = System.Net.WebRequest.Create(url)
let resp = req.GetResponse()
let stream = resp.GetResponseStream()
let reader = new System.IO.StreamReader(stream)
let html = reader.ReadToEnd()
resp.Close()
html
let getWords s = String.split [ ' '; '\n'; '\t'; '<'; '>'; '=' ] s
let getStats site =
let url = "http://" + site
let html = http url
let words = html |> getWords
let hrefs = html |> getWords |> List.filter (fun s -> s = "href")
(site,html.Length, words.Length, hrefs.Length)
Notare l'identificatore words nella definizione di getStats. Questo identificatore accetta il valore html restituito dall'URL e vi applica la funzione getWords. La definizione potrebbe anche essere scritta nel seguente modo:
let words = getWords html
Le due definizioni sono identiche. Tuttavia, l'identificatore hrefs mostra la potenza dell'operatore pipeline in quanto è possibile combinare insieme un numero arbitrario di applicazioni. A questo punto, inviare l'elenco di parole risultante tramite la funzione List.filter, che accetta una funzione anonima per cercare la parole href e restituirla se l'espressione è ritenuta valida. Infine, i risultati della chiamata a getStats corrisponderanno a un'altra tupla, (string * int *int * int). La scrittura di questo codice utilizzando C# comporterà l'utilizzo di 15 righe di codice.
Nella Figura 2 viene fornito un ulteriore esempio della compatibilità di F# con .NET Framework, un tema che viene ripetuto nel seguente codice:
open System.Collections.Generic
let capitals = Dictionary<string, string>()
capitals.["Great Britain"] <- "London"
capitals.["France"] <- "Paris"
capitals.ContainsKey("France")
In questo codice, che non prevede null'altro che l'esposizione del tipo Dictionary<K,V>, viene illustrato come specificare i generics in F# (utilizzando parentesi ad angolo come in C#), come utilizzare gli indicizzatori in F# (utilizzando la sintassi con parentesi quadre, come in C#) e come eseguire i metodi .NET (utilizzando l'operatore "punto" e le parentesi, come in C#). In effetti, l'unica novità è rappresentata dalla modalità di assegnazione dei valori mutabili, che prevede l'utilizzo dell'operatore freccia sinistra. Questo è necessario in quanto F#, come la maggior parte dei linguaggi funzionali, riserva l'utilizzo dell'operatore equals per il confronto, in conformità con la nozione matematica secondo cui se x = y, x e y hanno lo stesso valore e non che y viene assegnato a x (è noto che i matematici sono soliti storcere il naso o ridacchiare in modo isterico all'idea che l'istruzione x = x + 1 possa essere vera in qualsiasi universo reale o immaginato).
Definizione di oggetti in F#
Ovviamente, non tutti gli sviluppatori .NET che si stanno avvicinando a F# adotteranno immediatamente i concetti funzionali. Infatti, molti degli sviluppatori C# o Visual Basic iniziati al linguaggio F# devono avere la certezza di poter tornare alle vecchie abitudini senza rovinare il codice. E in una certa misura questo è possibile.
Si consideri, ad esempio, la definizione della classe per il vettore bidimensionale illustrato nella parte superiore della Figura 3. Dall'osservazione di questa figura è possibile trarre un paio di considerazioni interessanti. Primo, notare che non esiste alcun corpo di costruttore esplicito; i parametri nella prima riga indicano i parametri con cui gli utenti costruiranno le istanze Vector2D, fungendo sostanzialmente da costruttore. L'identificatore length, insieme agli identificatori dx e dy, diventa un elemento privato all'interno del tipo Vector2D, mentre la parola chiave member indica i membri che devono essere disponibili all'esterno di Vector2D, tramite l'accesso alle proprietà .NET standard. In sostanza, nel codice F# viene dichiarato ciò che viene visualizzato nella parte inferiore della Figura 3 (come riportato da Reflector).

Figure 3 Variazioni di vettori in F# e C#
VECTOR2D IN F#
type Vector2D(dx:float,dy:float) =
let length = sqrt(dx*dx + dy*dy)
member obj.Length = length
member obj.DX = dx
member obj.DY = dy
member obj.Move(dx2,dy2) = Vector2D(dx+dx2,dy+dy2)
VECTOR2D IN C# (REFLECTOR>
[Serializable, CompilationMapping(SourceLevelConstruct.ObjectType)]
public class Vector2D
{
// Fields
internal double _dx@48;
internal double _dy@48;
internal double _length@49;
// Methods
public Vector2D(double dx, double dy)
{
Hello.Vector2D @this = this;
@this._dx@48 = dx;
@this._dy@48 = dy;
double d = (@this._dx@48 * @this._dx@48) +
(@this._dy@48 * @this._dy@48);
@this._length@49 = Math.Sqrt(d);
}
public Hello.Vector2D Move(double dx2, double dy2)
{
return new Hello.Vector2D(this._dx@48 + dx2, this._dy@48 + dy2);
}
// Properties
public double DX
{
get
{
return this._dx@48;
}
}
public double DY
{
get
{
return this._dy@48;
}
}
public double Length
{
get
{
return this._length@49;
}
}
}
Ricordare che F#, come la maggior parte dei linguaggi funzionali, promuove l'utilizzo di stati e valori immutabili. Questo appare evidente quando si esamina il codice nella Figura 3, in quanto tutte le proprietà sono di sola lettura e il membro Move non modifica il tipo Vector2D esistente ma ne crea uno nuovo dal Vector2D corrente e vi applica i valori in corso di modifica prima di restituirlo.
Notare inoltre che la versione F# è non solo thread-safe ma è completamente accessibile dal codice C# o Visual Basic tradizionale. Questo fornisce un metodo semplice per iniziare a imparare il linguaggio F#: è possibile utilizzarlo per definire oggetti business o altri tipi che si desidera o è necessario siano thread-safe e immutabili. Sebbene sia possibile creare tipi in F# che forniscano il l'insieme standard di operazioni modificabili (impostazione di proprietà e simili), un'attività di questo tipo richiede maggior lavoro e l'utilizzo della parola chiave mutable. Questo linguaggio offre esattamente le caratteristiche necessarie per gestire le problematiche relative alla concorrenza, che sono ormai all'ordine del giorno: immutabilità per impostazione predefinita, mutabilità quando necessario o si desidera.
La creazione dei tipi in F# è interessante, ma è anche possibile utilizzare F# per eseguire le stesse operazioni eseguibili con il codice C# o Visual Basic tradizionale, come la creazione di un'applicazione Windows Forms semplice e la raccolta di input dall'utente, come illustrato nella Figura 4.

Figure 4 Windows Forms con F#
#light
open System
open System.IO
open System.Windows.Forms
open Printf
let form = new Form(Text="My First F# Form", Visible=true)
let menu = form.Menu <- new MainMenu()
let mnuFile = form.Menu.MenuItems.Add("&File")
let filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
let mnuiOpen =
new MenuItem("&Open...",
new EventHandler(fun _ _ ->
let dialog =
new OpenFileDialog(InitialDirectory="c:\\",
Filter=filter;
FilterIndex=2,
RestoreDirectory=true)
if dialog.ShowDialog() = DialogResult.OK then
match dialog.OpenFile() with
| null -> printf "Could not read the file...\n"
| s ->
let r = new StreamReader(s)
printf "First line is: %s!\n" (r.ReadLine());
s.Close();
),
Shortcut.CtrlO)
mnuFile.MenuItems.Add(mnuiOpen)
[<STAThread>]
do Application.Run(form)
Qualsiasi sviluppatore che abbia familiarità con Windows Forms sarà in grado di comprendere rapidamente il codice riportato in questo esempio: creazione di un semplice modulo, compilazione di alcune proprietà, aggiunta di un gestore eventi e configurazione dell'applicazione per l'esecuzione fino a quando l'utente non fa clic sul pulsante di chiusura nell'angolo superiore sinistro. I componenti standard, come le applicazioni .NET, vengono eseguiti automaticamente, consentendo di rimanere interamente focalizzati sulla sintassi F#.
L'istruzione open opera in modo molto simile all'istruzione using di C#, rendendo sostanzialmente uno spazio dei nomi .NET disponibile per l'utilizzo senza qualificatori formali. Lo spazio dei nomi Printf è specifico di F#, dal punto di vista tecnico una porta del modulo OCaml con lo stesso nome. F# fornisce non solo una completa fedeltà alla libreria di classi di .NET Framework ma anche una porta estremamente semplice delle librerie OCaml, consentendo ai programmatori che hanno dimestichezza con tale linguaggio di lavorare senza problemi con .NET Framework. Per i curiosi, Printf risiede all'interno dell'assembly FSharp.Core.dll. Nulla impedisce di utilizzare System.Console.WriteLine: è solo una questione di preferenza estetica.
La creazione dell'identificatore form prevede l'utilizzo dei parametri denominati F#, il che equivale alla creazione dell'istanza dell'oggetto e all'esecuzione di una serie di chiamate a un insieme di proprietà per compilare tali proprietà con valori. Allo stesso modo è possibile creare l'identificatore dialog, scrivendo poche di righe di codice più sotto.
La definizione dell'identificatore mnuiOpen contiene un costrutto interessante, non del tutto sconosciuto agli sviluppatori che hanno familiarità con i delegati anonimi di .NET Framework 2.0 o le espressioni lambda di .NET Framework 3.5. Nella costruzione dell'EventHandler associato a Open MenuItem è presente una funzione anonima, definita utilizzando la sintassi:
Analogamente ai delegati anonimi, viene creata una funzione che viene richiamata quando la voce di menu viene selezionata, ma la sintassi è un po' complicata.
La definizione della parte EventHandler della definizione di MenuItem è una funzione anonima che ignora i due parametri passati, che corrisponde agli argomenti sender ed event nel tipo di delegato EventHandler standard. La funzione stabilisce che deve essere visualizzato un nuovo OpenFileDialog e che, quando si fa clic su OK, i risultati devono essere esaminati:
if dialog.ShowDialog() = DialogResult.OK then
match dialog.OpenFile() with
| null -> printf "Could not read the file...\n"
| s ->
let r = new StreamReader(s) in
printf "First line is: %s!\n" (r.ReadLine());
s.Close();
I risultati vengono esaminati utilizzando il pattern matching, una potente funzionalità del mondo dei linguaggi funzionali. All'apparenza simile all'istruzione switch/case di C#, la funzionalità pattern matching, come implica il nome, confronta un valore con diversi modelli, non tutti necessariamente valori costanti, ed esegue il blocco di codice con cui ottiene una corrispondenza. Nel blocco di corrispondenze illustrato di seguito, ad esempio, il risultato di OpenFile viene confrontato con due valori possibili: null, che indica che non è possibile aprire alcun file, o s, a cui viene assegnato qualsiasi valore non null e viene utilizzato come costruttore di StreamReader per aprire e leggere la prima riga del file di testo specificato.
Il pattern matching è una parte importante della maggior parte dei linguaggi funzionali e merita un'ulteriore spiegazione. Uno degli utilizzi più comuni di questo meccanismo è la combinazione con un tipo unione discriminata, che ricorda vagamente un tipo enumerato di C# o Visual Basic:
// Declaration of the 'Expr' type
type Expr =
| Binary of string * Expr * Expr
| Variable of string
| Constant of int
// Create a value 'v' representing 'x + 10'
let v = Binary("+", Variable "x", Constant 10)
Questo meccanismo viene comunemente utilizzato nei linguaggi funzionali per creare rappresentazioni di base dei linguaggi specifici dei domini che gli sviluppatori possono utilizzare per scrivere costrutti più complessi e potenti. Ad esempio, non è difficile immaginare l'estensione di questa sintassi per creare un linguaggio di calcolo completo che può essere ulteriormente esteso aggiungendo semplicemente nuovi elementi al tipo Expr. Tenere presente che la sintassi che utilizza il carattere * non indica l'utilizzo della moltiplicazione ma rappresenta un metodo standard tra i linguaggi funzionali per indicare un tipo costituito da più parti.
I linguaggi funzionali infatti vengono comunemente utilizzati per scrivere strumenti per la programmazione orientata a oggetti, come interpreti e compilatori, in cui il tipo Expr rappresenta l'insieme completo dei tipi delle espressioni del linguaggio. F# semplifica questo comportamento includendo due strumenti, fslex e fsyacc, progettati specificamente per raccogliere gli input dei linguaggi tradizionali, i file lex e yacc, e compilarli nel codice F# per agevolarne la modifica. Se si è interessati a esplorare questi strumenti, eseguire il download del programma di installazione di F#; in particolare, l'esempio Parsing nella distribuzione di F# standard fornirà un'interessante struttura di base da cui iniziare.
L'unione discriminata è solo uno dei vantaggi del pattern matching; il secondo è rappresentato dall'esecuzione delle espressioni, come illustrato nella Figura 5. La funzione rec nella definizione di eval è necessaria per indicare al compilatore F# che eval sarà richiamato ricorsivamente all'interno del corpo della definizione. Senza rec, F# si aspetterà di trovare una funzione nidificata locale denominata eval. La funzione getVarValue viene utilizzata per restituire alcuni valori predefiniti per le variabili; in un'applicazione reale, getVarValue esaminerebbe probabilmente un Dictionary per verificare la presenza di valori da restituire, come stabilito al momento della creazione della variabile.

Figure 5 Esecuzione di espressioni
let getVarValue v =
match v with
| "x" -> 25
| "y" -> 12
| _ -> 0
let rec eval x =
match x with
| Binary(op, l, r) ->
let (lv, rv) = (eval l, eval r) in
if (op = "+") then lv + rv
elif (op = "-") then lv - rv
else failwith "E_UNSUPPORTED"
| Variable(var) ->
getVarValue var
| Constant(n) ->
n
do printf "Results = %d\n" (eval v)
Quando eval viene chiamato, accetta il valore v e scopre che si tratta di un valore Binary. Questo valore corrisponde alla prima espressione secondaria che, a sua volta, associa il valore (lv, rv) ai risultati valutati delle parti sinistra e destra del valore Binary appena esaminato. Il valore senza nome (lv, rv) è una tupla, sostanzialmente un singolo valore di più parti, simile a un set relazionale o uno struct C.
Quando eval l viene chiamato per la prima volta, l dall'istanza Binary è un tipo Variable e, pertanto, la chiamata ricorsiva a eval viene confrontata con la diramazione del blocco del pattern-matching. Questo chiama a sua volta getVarValue, che restituisce il valore 25 hardcoded, che viene associato al valore lv. La stessa sequenza viene eseguita per r, che è una costante contenente il valore 10, che viene associato a rv. Viene eseguito il resto del blocco, un blocco if/else-if/else, che risulta abbastanza semplice per uno sviluppatore che abbia familiarità con il linguaggio C#, Visual Basic o C++.
L'elemento principale da notare è che ogni espressione restituisce un valore, anche all'interno del blocco del pattern-matching. In questo caso, il valore restituito è un valore integer, il valore dell'operazione, il valore recuperato dalla variabile o la stessa costante. Questo punto, più di ogni altro, sembra indurre in errore gli sviluppatori che hanno maggiore dimestichezza con la programmazione orientata a oggetti o imperativa in quanto in C#, Visual Basic o C++ la restituzione di valori è facoltativa e può essere ignorata anche quando specificata. Nei linguaggi funzionali come F#, per ignorare un valore restituito è necessario un idioma di codifica esplicito. In tali casi, i programmatori possono passare i risultati a una funzione denominata ignore, che esegue esattamente l'operazione implicita nel relativa nome.
Modalità asincrona in F#
L'analisi finora eseguita della sintassi F# fa sì che il linguaggio F# venga incluso in una delle due categorie seguenti: un linguaggio con costrutti funzionali relativamente semplici o una variazione più strana e più concisa dei tradizionali linguaggi orientati a oggetti compatibili con .NET (C#, Visual Basic o C++/CLI). Nessuna delle due rappresentazioni costituisce un'argomentazione convincente per l'adozione di F# in un ambiente aziendale.
Tuttavia, se si esamina la Figura 6, si potrà osservare che non è rappresentata nessuna delle due categorie sopra menzionate. A parte i caratteri ! che vengono visualizzati in un paio di posizioni e l'utilizzo del modificatore async, questo codice sembra relativamente semplice: caricamento di un'immagine grafica di origine, estrazione dei relativi dati, passaggio di tali dati a una funzione autonoma per la manipolazione (rotazione, inclinazione o altro) e scrittura dei dati in un file di output.

Figure 6 Modifica di un'immagine
let TransformImage pixels i =
// Some kind of graphic manipulation of images
let ProcessImage(i) =
async { use inStream = File.OpenRead(sprintf "source%d.jpg" i)
let! pixels = inStream.ReadAsync(1024*1024)
let pixels' = TransformImage(pixels,i)
use outStream = File.OpenWrite(sprintf "result%d.jpg" i)
do! outStream.WriteAsync(pixels')
do Console.WriteLine "done!" }
let ProcessImages() =
Async.Run (Async.Parallel
[ for i in 1 .. numImages -> ProcessImage(i) ])
Ciò che non è ovvio è che l'utilizzo del modificatore async trasforma questo codice in ciò che in F# viene chiamato flusso di lavoro asincrono (nessuna relazione con Windows Workflow Foundation), il che indica che ciascuna di queste operazioni di caricamento/elaborazione/salvataggio viene eseguita in thread paralleli da un pool di thread .NET.
Per semplificare, si consideri il codice nella Figura 7. In questa particolare sequenza vengono rappresentati in modo semplice e comprensibile i flussi di lavoro asincroni. Senza addentrarsi in troppi dettagli, evals è una matrice di funzioni in attesa di essere eseguite, ciascuna delle quali viene accodata per l'esecuzione in un pool di thread dalla chiamata Async.Parallel. Quando eseguite, diventa evidente che le funzioni all'interno di evals sono, in effetti, in un thread separato dalla funzione in awr (anche se in ragione della natura stessa del pool di thread del sistema .NET, è possibile eseguire alcune o tutte le funzioni di evals sullo stesso thread).

Figure 7 Esecuzione di funzioni in modalità asincrona
#light
open System.Threading
let printWithThread str =
printfn "[ThreadId = %d] %s" Thread.CurrentThread.ManagedThreadId str
let evals =
let z = 4.0
[ async { do printWithThread "Computing z*z\n"
return z * z };
async { do printWithThread "Computing sin(z)\n"
return (sin z) };
async { do printWithThread "Computing log(z)\n"
return (log z) } ]
let awr =
async { let! vs = Async.Parallel evals
do printWithThread "Computing v1+v2+v3\n"
return (Array.fold_left (fun a b -> a + b) 0.0 vs) }
let R = Async.Run awr
printf "Result = %f\n" R
Il fatto che queste funzioni vengono eseguite al di fuori del pool di thread .NET evidenzia ancora una volta che F# garantisce un'interoperabilità completa con il runtime sottostante. Il supporto per la libreria di classi di .NET Framework anche per aree tradizionalmente riservate all'implementazione specializzata (come il threading) nei linguaggi funzionali implica che il programmatore C# può utilizzare le librerie o i moduli F#, così come gli sviluppatori F# utilizzano le librerie C#. Infatti, in futuro, le funzionalità di F# come le attività asincrone saranno in grado di sfruttare le nuove librerie di .NET Framework, come la libreria di elaborazione delle attività disponibile nella libreria delle estensioni parallele.
Interazione con F#
Se non è del tutto evidente, sono ancora molti gli aspetti del linguaggio F# che occorre esplorare e che non è possibile esaminare in un unico articolo. In effetti, tra la nuova sintassi e il nuovo modo di pensare la programmazione (funzionale vs imperativa), occorre sottolineare che per lo sviluppatore medio orientato agli oggetti che abbia familiarità con il linguaggio C# o Visual Basic l'apprendimento di F# potrebbero richiedere tempo. Per fortuna, F# garantisce un'interoperabilità completa con il resto dell'ecosistema .NET, il che implica che è possibile sfruttare gran parte delle conoscenze e degli strumenti esistenti per portare F# nel proprio arsenale di codifica.
Tutte le librerie di base sono completamente accessibili allo sviluppatore F# e, poiché F# supporta alcuni aspetti dello sviluppo imperativo e orientato a oggetti, è consigliabile valutare l'opportunità di utilizzare la modalità interattiva di F# come metodo per apprendere sia la sintassi F# che i dettagli di Windows Presentation Foundation, Windows Communication Foundation o Windows Workflow Foundation senza dover sospendere i cicli di compilazione.
Come già accennato, gli sviluppatori possono scrivere oggetti business in F# per l'utilizzo in altre parti del relativo codice dell'applicazione. Perché la creazione dei tipi F# produce classi che sono, per la maggior parte, identiche ai relativi equivalenti in C# o Visual Basic, le librerie di persistenza come NHibernate renderanno permanente i tipi F# senza problemi, garantendo un'integrazione completa di F# con le applicazioni aziendali funzionanti.
Il semplice apprendimento del linguaggio F# semplificherà la comprensione di numerose funzionalità delle versioni future di C# e Visual Basic, in quanto molti dei concetti e delle idee alla base di queste funzionalità, tra cui generics, iteratori (la parola chiave yield in C#) e LINQ, hanno radici funzionali e sono frutto delle ricerche condotte dal team di F#. A prescindere da come la si consideri, la programmazione funzionale è ormai una realtà affermata.
Ted Neward è un consulente indipendente specializzato in sistemi aziendali su larga scala. È autore e coautore di numerosi libri, Microsoft MVP per l'architettura di soluzioni, Direttore Tecnico BEA, relatore di INETA e docente di PluralSight. È possibile contattare Ted all'indirizzo
ted@tedneward.com o visitare il suo blog all'indirizzo
blogs.tedneward.com.