A presentarci
F#, il nuovo linguaggio della famiglia .NET che riunisce i paradigmi imperativo, O-O e funzionale, dopo
Robert Pickering troviamo stavolta, oltre a
Don Syme — il
papà del CLR e dei generics .NET oltre che di
F# stesso — anche la gradita presenza del Prof.
Antonio Cisternino dell'Università di Pisa, il principale maintainer del
sito dedicato al libro (creato con
SharePoint) dal quale è possibile scaricare il
codice di esempio dei capitoli. Non c'è che dire, l'infinita superiorità del framework .NET è in grado di conquistare anche un mondo fortemente ostile alle tecnologie Microsoft come quello accademico.
Il lavoro di review è stato affidato a
Tomas Petricek, un
MVP del quale vi consiglio caldamente la sua
introduzione ad F#. Il testo si divide in due parti: i primi dieci capitoli, con chiaro piglio didattico, illustrano le basi del linguaggio, delle tecniche di programmazione utilizzabili in
F# e delle librerie (giova ricordare che una parte di esse deriva direttamente da
OCaml, con le opportune
differenze); i rimanenti nove mostrano un buon numero di tecniche applicate, dalle
Windows Forms allo sviluppo Web, alla programmazione asincrona e concorrente, all'accesso ai dati (senza tralasciare
LINQ), all'interoperabilità con COM ed alla creazione di
parser ed analizzatori lessicali, per finire con le tecniche di ingegneria del software.
Il
primo capitolo ha carattere puramente introduttivo ed illustra la genesi di
F#, oltre ai contenuti ed ai destinatari del libro. Nel
secondo capitolo facciamo invece la conoscenza con
F# Interactive (
fsi.exe), una shell per l'interpretazione a linea di comando, che può essere eseguita anche come add-in di Visual Studio; e con il primo programma in
F# ed i primi costrutti. Il
setup di F# aggiunge anche un'utile voce di menu contestuale
Run with F# Interactive per l'esecuzione degli script. Come primo approccio, conviene usare l'istruzione
#load per eseguire in
F# Interactive gli script di esempio
.fsx proposti; il comando accetta come argomento il percorso completo di un file, da specificare come stringa
verbatim di .NET, così:
<span style="font-size:1.0em">
#load @"<img src="./nbbc/smileys/angry.gif" width="16" height="16" alt="D:" title="D:" class="bbcode_smiley" />/Things To Do/Not Subject To Postponement/KillGosling.fsx";;</span>
Tornando all'esempio del capitolo, per quanto banale, costituisce l'occasione per introdurre importanti concetti, approfonditi nel seguito, quali il sistema dei tipi di
F#, la
type inference ed i costruttori di tipo; oltre alle strutture dati, in particolare le tuple, ovvero un certo numero di espressioni, raggruppate insieme per formare una nuova espressione. Nella seconda parte del capitolo, un nuovo esempio ci mostra come fare uso in codice
F# di librerie parte del framework .NET, come
System.Windows.Forms e
System.Net. E' evidente, sin da questo capitolo, l'approccio seguito dal testo: presentare codice di esempio, commentarlo ed approfondire i concetti esposti. Numerosi box sparsi nel testo permettono al Lettore di concentrarsi sull'esempio proposto, per poi tornare eventualmente all'approfondimento teorico.
La caratteristica distintiva di
F# rispetto agli altri linguaggi .NET, ovvero il supporto alla programmazione funzionale, costituisce l'oggetto del
terzo capitolo. Dopo un'introduzione da manuale universitario ad operatori aritmetici e stringhe, entriamo nel vivo delle caratteristiche funzionali di
F# con la trattazione delle liste, per le quali viene presentato uno dei costrutti fondamentali di
F#, il
pattern matching: fondamentalmente, è un potente strumento per discriminare ed estrarre informazioni dagli elementi di una lista. Una seconda parte del capitolo entra nel vivo della programmazione funzionale, mostrando le operazioni possibili sui
function values oltre ad argomenti quali le
funzioni anonime ed il basilare
pipe-forward operator, un costrutto utile soprattutto perché allevia il lavoro del compilatore nel dedurre l'inferenza di tipo.
La parte finale del capitolo è dedicata ad approfondimenti sul
pattern matching e sulla definizione di tipi. Si fa notare, soprattutto, il meraviglioso esempio per elencare ricorsivamente il contenuto di tutti i file in un percorso del disco:
<span style="font-size:1.0em">
#light
open System.IO
let rec allFiles dir =
Seq.append
(dir |> Directory.GetFiles)
(dir |> Directory.GetDirectories |> Seq.map allFiles |> Seq.concat)
</span>
Oltre all'interoperabilità con le librerie .NET, al
pipelining, alla ricorsione, all'uso delle funzioni come valore ed all'esecuzione
lazy, quest'esempio è soprattutto una pietra miliare della
type inference in
F#: riuscite a vedere una singola dichiarazione di tipo, nel codice presentato? Rispetto al precedente
testo di Pickering troviamo, oltre ad una maggiore chiarezza espositiva, anche un continuo confronto sul come la programmazione funzionale consente di estendere e semplificare i costrutti di quella O-O.
Come più volte ricordato, con
F# il framework .NET, il primo ed unico multilinguaggio, diventa oggi multiparadigma, ed il
quarto capitolo copre gli aspetti inerenti la programmazione imperativa; non senza aver ricordato, però, i benefici di un paradigma privo di
side effects quale quello funzionale: non solo un minor numero di bug e la possibilità di creare programmi
compiled on demand, ma soprattutto, la stesura di codice che può essere eseguito
as is in maniera concorrente, perché inerentemente
thread-safe. Il capitolo comincia presentando i tre tipi di ciclo di
F# (
for, while e sequenza) ed i
mutable, in buona sostanza variabili (o funzioni…) il cui valore può essere assegnato nel corso del programma, insieme con gli array. Un lungo paragrafo è dedicato alle
collection .NET contenute in
System.Collections.Generic, con il loro uso ed i loro equivalenti in
F#. Dopo aver mostrato la
gestione delle eccezioni, l'uso base delle librerie .NET di I/O ed i (rari) casi in cui occorre avere a che fare con valori
null in
F#, il capitolo si chiude mostrando alcune tecniche per evitare
side effects pur utilizzando i costrutti imperativi del linguaggio.
I tipi, e gli ineguagliati meccanismi
generic e di inferenza di tipo di
F#, sono l'oggetto del
quinto capitolo. La prima, importantissima caratteristica del linguaggio (solitamente mancante in altri linguaggi che adottano il paradigma funzionale) è l'
automatic generalization, ovvero la capacità del compilatore
F# di riconoscere, in maniera per l'appunto
automatica, che un tipo è generico. Quello che succede nella pratica è che il compilatore
F# usa l'
automatic generalization quando una definizione di funzione con
let o
member non
forza il tipo delle variabili di input/output. Fatta questa premessa, il testo illustra nel dettaglio le funzioni
generic disponibili in
F#, terminando con un paragrafo su come usarle nel proprio codice. Passa poi ai tipi del framework .NET, cominciando con la differenza fra
value e reference types, per poi dedicare una particolare attenzione al
casting. Termina con una sezione di
troubleshooting della
type inference, con particolare riferimento alla
value restriction.
Per quanto, ovviamente, il precedente introduca l'argomento, è nel
sesto capitolo che viene trattato il paradigma O-O in
F#. Per la verità, in
F# molti costrutti dell'OOP, in particolar modo l'ereditarietà, tendono ad avere una minore importanza, grazie alla disponibilità di alternative come i
function values, le
partial implementations ed il
delegation pattern. Ciò nonostante, soprattutto per compatibilità con il passato,
F# offre pieno supporto alla programmazione O-O, che il capitolo presenta distinguendo ordinatamente fra le
feature per i tipi concreti (classi) e quelle per i tipi astratti (interfacce). Largo spazio viene riservato all'
overloading ed alla gestione dello stato, possibile in
F#, a differenza che nei linguaggi funzionali cosiddetti
puri, tramite i
mutable. Il testo parla anche dei
module, la keyword di
F# per definire classi statiche, ed una tecnica tanto poco usata quanto consigliabile ai novizi, ma a volte utilissima, quale la possibilità di estendere tipi esistenti con nuovi metodi. Apprendiamo, infine, come in compilazione tutti i tipi
F# vengono trasformati in tipi standard .NET, e come forzare l'uso di tipi .NET di peculiare uso come
struct,
delegate ed
enum.
Un concetto chiave dell'OOP, l'incapsulamento, è rimandato al successivo
settimo capitolo, insieme alle modalità di packaging del codice
F#. E' un capitolo, a dirla tutta, che presenta poche peculiarità di
F#, giacché gran parte delle caratteristiche presentate sono già disponibili nel framework .NET. I meccanismi utilizzati per l'incapsulamento, tanto per cominciare, sono quelli tipici di .NET, come le definizioni locali ed i
livelli di accessibilità; sono invece caratteristici di
F# i
signature file (
file di firma), che definiscono le funzionalità realizzate da un file di implementazione corrispondente. L'uso di namespace e moduli è invece del tutto analogo ai rimanenti linguaggi .NET; anche il compilatore
fsc.exe, che viene presentato per la prima volta in questo capitolo con le sue opzioni ed in affiancamento all'interprete a linea di comando, presenta molte analogie con
csc.exe. La creazione di assembly, la loro firma e registrazione nella
Global Assembly Cache (GAC), e le modalità di packaging delle risorse, installazione (con un interessante cenno a
WiX) e deployment delle applicazioni non presentano, anch'esse, alcuna novità per lo sviluppatore .NET.
L'
ottavo capitolo ha valore di miscellanea, presentando alcune tecniche comuni di programmazione in
F#. Gli argomenti coperti dal capitolo cominciano con la comparazione fra
generic, e uso di valori precalcolati e caching per aumentare la velocità di esecuzione. Una parte di estremo interesse, per lo sviluppatore .NET in generale, è la deallocazione delle risorse. Si comincia da quelle per definizione
non-managed: handle dei file, connessioni di rete, thread, oggetti della GUI, e più in generale quelle che non hanno a che fare con la memoria allocata su
stack ed
heap. Ogni oggetto che usa queste risorse dovrebbe implementare l'interfaccia
System.IDisposable; oltre a mostrare vari esempi, il testo raccomanda l'uso di uno strumento come
CLRProfiler lì dove il
garbage collector non arriva: elaborazioni di lunga durata, e
callback inattivi. Paradossalmente, però, sono proprio le risorse gestite dal
garbage collector, in particolare la memoria sullo
stack, a richiedere la maggiore attenzione (gli autori ricordano come è pressoché impossibile il recupero da una
StackOverflowException); in questo caso sono d'aiuto due tecniche di capitale importanza in
F#, la
tail recursion per le funzioni ed il
continuation-passing style, utile soprattutto lavorando con gli alberi. Un paragrafo sulla gestione degli eventi, che in
F# possono essere usati a guisa di ogni altro valore, chiude il capitolo.
Già
Robert Pickering ci aveva parlato nel suo testo delle feature di meta-programmazione che costituiscono, a tutti gli effetti, un
quarto paradigma implementato da
F#, la
Language-Oriented Programming. Con un doveroso omaggio, gli autori riprendono la sua intuizione nel
nono capitolo, mostrandoci come
F# è atto a trattare le rappresentazioni
concreta,
astratta e
computazionale di un linguaggio. Come esempio di rappresentazione concreta di un linguaggio si parte da
System.Xml, per mostrare poi quant'è facile lavorare in
F# con una rappresentazione astratta come gli AST (
Abstract Syntax Trees). L'estensione dei
pattern matching dalle semplici funzioni valore ai tipi dà luogo ai cosiddetti
active pattern, mentre la generalizzazione delle
sequence expression porta alle
computation expression, impropriamente chiamate workflow. Di queste ultime, gli autori ci mostrano analogie e differenze con le
Haskell monad. Come esempio ci viene mostrata un'espressione in grado di lavorare su distribuzioni di interi, riprendendo un esempio tratto dal paper “
A Programming Language for Probabilistic Computation”.
Le capacità di astrazione di
F# raggiungono i loro vertici (attuali) con la
reflection e le
quotation. Se la prima è un'estensione delle ben note capacità del framework .NET di recuperare a runtime informazioni su assembly, definizioni di tipi e firme dei relativi metodi e proprietà, le
quotation sono l'ennesima potenza delle
lambda expression già disponibili in C# 3.0, e vengono usate per ottenere una rappresentazione delle espressioni
F# sotto forma di AST. Gli esempi mostrati, fra i quali un reader di 50 linee di codice in grado di generare una collezione di oggetti da un semplice CSV, sono impressionanti; ma le
quotation si sono spinte ben oltre, fino a poter usare
F# come generatore di linguaggi:
meta-programmazione SQL con LINQ, piuttosto che la trasformazione in JavaScript operata dagli
F# Web Tools descritti in “
Ajax-style client/server programming with F#”. Vedremo entrambi nel seguito del testo; per ora, basti sapere che il
solito Petricek ha reso disponibile un
F# quotations visualizer per dare una rappresentazione visuale alle
quotation tramite
WinForms.
Coerentemente con la vocazione multiparadigma di
F#, il
decimo capitolo, nominalmente dedicato alle librerie di .NET ed
F#, funge al tempo stesso da riepilogo, da miscellanea e da introduzione. Riassume infatti ordinatamente le librerie presentate nei capitoli precedenti, ne tratta brevemente di nuove, e funge da
ponte verso le rimanenti 300 e passa pagine del testo, dedicate alle applicazioni pratiche, che sfruttano tali librerie. Dalle tabelle di overview iniziali scopriamo anche cosa
non viene trattato nel testo; si tratta di namespace importanti come
Microsoft.Win32,
System.Security,
System.CodeDom,
System.Globalization ed il nuovissimo
System.Media. Il capitolo completa poi la trattazione dei precedenti in materia di stringhe, dedicando particolare attenzione al namespace
System.Text.RegularExpressions, e dedica un paio di pagine ad alcune strutture dati avanzate.
Il capitolo termina presentando le funzionalità di calcolo numerico offerte da
Microsoft.FSharp.Math e dando un'introduzione più organica al namespace
Microsoft.FSharp.Reflection. In chiusura, gli autori ci tengono a segnalare alcune librerie che per ragioni di spazio non hanno trovato sistemazione nel testo, ma non per questo meno importanti:
Robotics Studio,
XNA,
GTK#,
Extreme Optimization,
Irrlicht,
WPF (ma all'uso di
WPF da
F# è dedicato
un intero blog) e
WCF.
La flessibilità di
F# e la sua interazione con le librerie .NET fanno sì che esso possa essere facilmente utilizzato per creare applicazioni grafiche
WinForms, oggetto dell'
undicesimo capitolo. Per quanto
F# Interactive offra un'opportunità unica di vedere una
WinForm costruita
pezzo per pezzo, a causa dei lunghi listati l'uso di
fsc.exe diventa predominante per vedere all'opera gli esempi proposti nel
codice associato al testo. Dopo i primi, semplici esempi, che però permettono agli autori di illustrare argomenti quali la creazione ed il layout dei controlli piuttosto che il
device context, il testo ci mostra due interessanti esempi, con elementi facilmente adattabili alla programmazione reale. In primo luogo, il pattern
Model-View-Controller (MVC) è alla base di un simulatore di plotter di dati acquisiti da sensori, che ci permette anche di apprezzare l'interessante controllo
PropertyGrid, che fa uso della
reflection per recuperare a runtime le proprietà di un oggetto e modificarle in maniera visuale. Uno degli esempi più classici e belli della matematica, l'insieme di
Mandelbrot, fa invece uso della programmazione multithread (approfondita in un capitolo successivo) per gestire i complessi calcoli necessari alla visualizzazione.
Gli argomenti del nono capitolo, in particolare gli AST e le
discriminated union, vengono ripresi nel
dodicesimo capitolo, che tratta un argomento assai congeniale alle capacità di astrazione fornite da
F#: le rappresentazioni simboliche. Vengono presentati due classici esempi: un valutatore di espressioni (sul modello di Matlab e
Mathematica) ed un verificatore di circuiti hardware basato sulla logica preposizionale. Il primo esempio usa un analizzatore lessicale ed un parser generati da due tool forniti dal setup di
F#,
fslex.exe ed fsyacc.exe, che sono oggetto essi stessi di un successivo capitolo. Anche la rappresentazione grafica mediante
WinForms, con le strategie scelte per visualizzare opportunamente frazioni ed esponenti, merita un'attenta lettura.
Il secondo esempio è una delle aree di applicazione più consolidate per la programmazione funzionale, e si basa sulla rappresentazione canonica delle preposizioni logiche mediante
Bynary Decision Diagram (BDD), oltre che su un precedente verificatore automatico di teoremi,
HOL88. Viene dapprima mostrato come le
tradizionali tabelle di verità sono efficaci, nel senso che permettono di valutare correttamente le proprietà dei circuiti, ma al tempo stesso sono inefficienti: la verifica di un semplice circuito comincia ad essere
pesante, in termini computazionali, già solo con cinque ingressi. I
BDD, risalenti ai tardi anni Ottanta, sono invece dei verificatori efficienti, basati solamente su condizionali
if…then…else, che usano tecniche di precalcolo e memorizzazione dei risultati intermedi. I risultati mostrati nel testo sono impressionanti: un problema di verifica con 74 variabili booleane, che con le tabelle di verità richiederebbe di effettuare 2 elevato alla 74 casi di test, con i
BDD viene portato a termine in pochi secondi.
Il
tredicesimo capitolo parla di una delle caratteristiche più interessanti, non solo di
F#, ma dell'intero framework .NET: la programmazione concorrente. Dopo una breve introduzione alla terminologia, viene illustrato l'uso della classe
System.ComponentModel.BackgroundWorker per la programmazione concorrente
sincrona, e come incapsularla. In materia di programmazione concorrente
asincrona,
F# offre un potente strumento negli
asynchronous workflow, costruiti intorno alla classe
<span style="font-size:1.0em">Microsoft.FSharp.Control.Async</span>
. La necessaria gestione dei thread viene enormemente facilitata dalla classe
System.Threading.ThreadPool, che fornisce
out-of-the-box la propagazione delle eccezioni e la cancellazione dei thread, e permette una gestione del ciclo di vita delle risorse mediante il semplice utilizzo della parola chiave
use. Il testo mostra come creare un'applicazione asincrona per il processing in parallelo di un elevato numero di immagini, con una panoramica delle API .NET per la gestione concorrente degli stream di I/O, delle richieste Web, dell'accesso a database ed XML, ed approfondendo l'uso di alcune primitive di
Async.
Si passa poi ad esaminare la programmazione concorrente mediante scambio di messaggi, con un esempio di crawler Web. Infine, a dispetto delle classi di alto livello di così semplice ed affidabile uso del framework .NET, passa ad illustrare le primitive di basso livello per la gestione della concorrenza
shared-memory, che è necessario (e difficile) gestire quando si vogliono ottenere particolari miglioramenti nelle prestazioni. Il
.NET Memory Model ed argomenti come i lock e l'evitamento delle
race condition, i
mutex ed i semafori sono il filo conduttore di questa panoramica del namespace
System.Threading, che termina illustrando l'uso di una classe di stupefacente utilità quando, com'è comune ad una struttura dati, si deve accedere in maniera concorrente ad oggetti che vengono scritti una volta sola, e letti più volte nel corso dell'esecuzione: la classe
ReaderWriterLock.
Un linguaggio versatile come
F# offre insospettate feature per la programmazione Web: esse sono l'argomento del
quattordicesimo capitolo. L'uso dei
socket .NET permette di scrivere, sin dal primo paragrafo, un semplice ma completo Web server con
settanta-linee di
codice-settanta compresi i commenti; il server gestisce direttamente tramite richieste HTTP, di cui effettua il parsing con le
espressioni regolari, ogni tipo di contenuto statico (grazie ad un dictionary estensibile dei tipi MIME) ed opera in modalità asincrona. Parte della ricchezza di questo testo consiste nel fatto che parla di
F#, ma permette di approfondire la conoscenza di quasi tutti i principali aspetti del framework .NET: i successivi paragrafi sono una ricapitolazione della tecnologia ASP.NET. In verità, buona parte del capitolo è basata sul lavoro svolto da
Petricek con l'
F# CodeDOM, usato per far generare codice
F# alle pagine ASP.NET.
Nulla manca, dalla configurazione del web server tramite il
web.config al
code-behind, dai
server control (comprese naturalmente le
data grid, e con un cenno particolare per i
validator control) agli
User Controls .ascx, dall'accesso ai dati (con un accenno a
LINQ, oggetto del successivo capitolo) alle
direttive di pagina, dai
Custom Control al
debugging, dal modello degli
eventi a quello dei
provider.
L'
onda lunga di
AJAX non ha risparmiato nemmeno
F#, con la nascita ad opera del solito
Petricek del progetto collaborativo
F# Web Tools; allo stato, questo progetto usa le feature
language-oriented di
F#, in particolare le
quotation, per generare dinamicamente codice JavaScript, con l'obiettivo di migrare al più presto a
Silverlight ed al
Dynamic Language Runtime. Il testo dedica solo una breve pagina al progetto, che era ai suoi esordi nel momento in cui è stato scritto; in compenso, chiude in bellezza con l'uso da
F# dei
Web Services, referenziando nello specifico quelli di
TerraServer e
WebserviceX.NET, e mostrando la loro invocazione asincrona: un task in generale enormemente facilitato dall'uso di Visual Studio, che all'atto della referenziazione di un Web Service genera sia proxy sincroni che asincroni per esso.
Il
quindicesimo capitolo è una disamina di tutte le tipologie di accesso ai dati possibili da
F#, cominciando dai costrutti per le query di strutture dati direttamente in memoria. I metodi del tipo
F# Seq riflettono invero appieno l'approccio di
LINQ: interrogazione e manipolare strutture dati direttamente in memoria, utilizzando una sintassi
SQL-like che liberi completamente il programmatore dalla necessità di imparare un secondo linguaggio quando deve operare con l'accesso ai dati. La versatilità di questo approccio, applicabile ad una sorgente dati la più generica possibile, fa sì che sia possibile ottenere praticamente gratis il parallelismo: è quello che è stato fatto, in effetti, con
PLINQ. La nascita di
LINQ porterà quindi ad un nuovo paradigma nell'accesso e nella manipolazione dei dati, ma non farà certo venire meno la necessità della loro persistenza: il capitolo non trascura quindi l'interazione con basi di dati relazionali offerta da
ADO.NET, offrendo persino una breve panoramica del modello relazionale, probabilmente ridondante per molti Lettori.
Anche in questo caso, l'argomento è alquanto vasto, ma il testo fornisce un'ottima panoramica, dalle connessioni ai
DataSet ed ai
DataAdapter, senza trascurare le
DataGrid, le
stored procedure o la generazione di
DataSet tipizzati con
xsd.exe. Non poteva mancare, naturalmente, un paragrafo dedicato alle straordinarie funzionalità di Visual Studio per lavorare in maniera visuale su un qualsiasi database per il quale sia disponibile un provider ADO.NET. Tuttavia, viviamo in un mondo in cui
LINQ è in mezzo a noi, quindi è il momento di pensare al passaggio ad un ORM del calibro di
LINQ to SQL (per quanto, naturalmente, lo strato più basso di questo ORM faccia uso di ADO.NET).
Il cuore delle funzionalità ORM offerte da
LINQ to SQL è il tool a linea di comando
SqlMetal, in grado di generare una classe, ereditata da
DataContext (la classe principale del namespace
System.Data.Linq), che rappresenta l'intero database, sulla quale è poi possibile lavorare con l'
O/R Designer di Visual Studio. L'uso delle
quotation di
F# fornisce una sintassi ancor più compatta e naturale per l'interrogazione del modello ad oggetti. A dispetto delle straordinarie funzionalità di ORM, come ricordavo in apertura
LINQ è stato pensato come modello di accesso unificato alle sorgenti dati le più disparate possibili: il capitolo termina mostrandocene per l'appunto la versione per manipolare dati in formato XML,
LINQ to XML, una
rivoluzione copernicana rispetto al classico XML DOM che non solo permette di costruire documenti XML in modalità funzionale, ma anche di interrogarli con le query
LINQ, così simili alla sintassi SQL, invece che tramite XPath.
Paradossalmente, a parte
LINQ, l'aspetto più noto di
F# sono le astrazioni sui tipi e le capacità di manipolazione del codice a runtime che ha
donato a
C# 3.0. Un linguaggio come
F# si presta infatti straordinariamente alle operazioni proprie della teoria dei compilatori; in particolare, all'analisi lessicale ed al
parsing di espressioni, oggetto del
sedicesimo capitolo che approfondisce l'uso dei tool
fslex.exe ed fsyacc.exe precedentemente incontrati. Dopo una breve introduzione al
parsing di semplici file su linee separate con
System.Text.RegularExpressions, il capitolo affronta separatamente l'analisi lessicale ed il parsing. Si parte con la
tokenizzazione tramite FsLex, affrontando l'uso del tipo
Microsoft.FSharp.Tools.FsLex.LexBuffer ed i pattern per la creazione di regole.
Il
parsing recursive-descent ed un box di introduzione alle grammatiche
context-free ed alla
Backus-Naur Form (BNF) fungono da ponte per il parser
LALR implementato da FsYacc. Come esempio viene mostrato un semplice, ma completo linguaggio
BASIC-like, Kitty, inventato dagli autori; i relativi paragrafi affrontano anche la risoluzione dei due principali tipi di conflitto per una grammatica (
reduce-reduce e
shift-reduce), la precedenza degli operatori e l'associatività, ed introducono l'utilissimo switch
<span style="font-size:1.0em">-v</span>
di
fsyacc.exe, che permette di produrre un estratto leggibile degli stati del parser. Il capitolo si chiude con l'uso dei cosiddetti
combinatori per il
parsing di dati binari.
Per quanto .NET ed
F# siano il futuro (ed il presente), l'eredità di C e COM è ancora ben viva nel mondo dello sviluppo. Il
diciassettesimo capitolo, dedicato all'interoperabilità fra
F# e codice
unmanaged, è comunque uno dei più interessanti e piacevoli del testo, un avvincente
tuffo nel passato (e nel presente) che ci ricorda dov'era il 90% del mercato di ieri, per farci capire dove sarà il 90% del mercato di domani. La succinta introduzione iniziale ai meccanismi del CLR ed alla gestione della memoria a runtime, un argomento di assoluta importanza per l'interoperabilità fra differenti linguaggi di programmazione, è impagabile, e degna del
Richter. La parte sulla
COM Interoperability inizia anch'essa con un'introduzione al modello per componenti che per un decennio ed oltre ha fatto la storia della programmazione Microsoft; ricordare
IDispatch o
IUnknown,
AddRef o
QueryInterface, le OLE o le
type library riporta ad un'epoca gloriosa, in cui il computer lasciava i data center per arrivare sulle scrivanie dell'umanità.
Per quanto quel modello sia stato sorpassato dai tempi, alcune sue geniali caratteristiche li precorrevano, e sono state alcune delle basi da cui è nato il CLR: l'interoperabilità binaria, fondamento della natura multilinguaggio di .NET; il caricamento dinamico dei componenti; la
reflection, nata soprattutto per gli ambienti di scripting ed oggi portata ai confini della programmazione conosciuta. Dopo un box sull'uso del registro come
store per i metadati COM, è tempo di passare ai due tipi di wrapper, che permettono di incapsulare un mondo all'altro: il
COM Callable Wrapper (CCW) da un lato, il
Runtime Callable Wrapper (RCW) dall'altro. Con impareggiabile simmetria, due coppie di tool del .NET Framework permettono di generare automaticamente questi wrapper: da un lato,
tlbimp.exe porta i componenti COM (data la loro
type library lì dove lo sviluppo non era mai giunto prima, coadiuvato da
aximp.exe per gli ActiveX; dall'altro, il vertice che nessuno può eguagliare ben si presta a ridursi in forma di
type library grazie a
tlbexp.exe, o a
regasm.exe che in più effettua anche la registrazione di un assembly come componente COM.
Il testo mostra come usare da codice
F# in tal guisa il controllo ActiveX che implementa il Flash Player, e come esporre invece come componente COM un semplice assembly
F#. Per quanto molto sia possibile con i quattro tool citati, in alcuni casi, come nella specifica del ProgID, il namespace
System.Runtime.InteropServices con le sue
annotation (un'idea copiata, ma non eguagliata) fornisce un impareggiabile aiuto. L'interoperabilità è materia complessa, ma chi programma insieme al 90% dell'umanità non è mai solo: la Microsoft fornisce le versioni .NET
managed dei
componenti di Office, delle
librerie DirectX e del
Web Browser control.
Un altro lato dell'interoperabilità .NET è più complesso, ma ancor più affascinante. La
COM Interoperability, con le sue
facility, è limitata al mondo di Windows ed all'implementazione Microsoft dello
standard ISO CLI (Common Language Infrastructure). In altri tempi, voci interessate e nemiche di un'umanità oggi in grado di usare un computer hanno cercato di far credere che .NET fosse limitato alle piattaforme Microsoft; come se il concetto stesso di
limite, possa essere applicato alla piattaforma .NET. È stata invece Microsoft stessa a porre le basi per il .NET multi-piattaforma, includendo un meccanismo standard di interoperabilità disponibile per tutte le implementazioni del
CLI, come Mono (o come
Rotor, ancora una volta uno sforzo Microsoft): sto parlando, naturalmente, del
Platform Invoke. Il modello di P/Invoke (com'è familiarmente chiamato) prevede il caricamento a runtime di librerie nello spazio di lavoro del programma, permettendo al codice
managed di invocare le funzioni esportate da tali librerie.
P/Invoke permette di ottenere tramite una semplice chiamata di sistema l'indirizzo della funzione in memoria, ma rimane in carico al programmatore la convenzione di chiamata (il testo dedica molta attenzione a questo argomento, distinguendo in particolare fra
stdcall e
cdecl) e, soprattutto, il
marshalling dei parametri. Gli autori approfondiscono una serie di argomenti, dalle semplici chiamate
extern al layout in memoria delle strutture dati, dal
marshalling dei parametri (con una particolare attenzione per il tipo stringa) ai puntatori a funzione, terminando con un esempio, sufficientemente completo, di
memory mapping per l'accesso paginato in memoria di file. L'accento è soprattutto sulla natura dichiarativa di
P/Invoke, basata sulle
annotation, che tanto semplifica il lavoro allo sviluppatore; ma anche sulla necessità, quando si lavora con
P/Invoke, di operare con privilegi elevati, e pertanto di ridurne al minimo l'uso e di
concentrarlo in un numero di assembly verificabili che fungono da
gateway verso il CLR.
Gli ultimi due capitoli del testo sono pronti ad affrontare i paradigmi dell'ingegneria del software con
F#: il primo, il
diciottesimo capitolo, ci parla di debugging e testing (in particolar modo col framework
NUnit). Prima di
NUnit, però, il capitolo affronta il debugging con il debugger integrato in Visual Studio, ed usando
roughly F# Interactive. Il debugger di Visual Studio è uno strumento straordinario (nella versione 2005 e successive è perfino in grado di debuggare applicazioni concorrenti), ed il testo ne evidenzia in particolare i
conditional breakpoint e la possibilità di
attaccarsi ad un processo in esecuzione. Il CLR a sua volta offre un buon numero di servizi di debugging, quali il
tracing dello
stack piuttosto che contatori di prestazioni sulle risorse, gestione dei processi del sistema operativo o accesso al log degli eventi, accessibili tramite le classi del namespace
System.Diagnostics. Il testo si concentra soprattutto sulle classi relate al debugging, mostrando il meccanismo degli
Assert e la
gestione del debugger.
La parte su
F# Interactive è piuttosto breve e si concentra sulle direttive e sui metodi messi a disposizione, oltre che sulle modalità di compilazione del codice
F#: va detto, infatti, che per quanto
F# Interactive appaia all'utente come un interprete, è in realtà un compilatore:
entra in azione appena la
type inference fornisce informazioni sufficienti per la risoluzione dei tipi di un frammento di codice, lo compila
al volo in un assembly generato dinamicamente e lo esegue. Viene infine mostrato l'uso di
NUnit e, più in generale, del
Test-Driven Development (TDD) per identificare un bug in una semplice funzione di esempio; questa offre tuttavia l'occasione per parlare degli attributi aggiuntivi offerti dal framework per supportare la documentazione e la classificazione dei casi di test. Vengono solo accennata la presenza di altri strumenti di unit testing disponibili per .NET, quali
Visual Studio Team System ed
NCover.
Il
diciannovesimo ed ultimo capitolo illustra le opinioni degli Autori in merito alla progettazione di librerie
F#. Un primo approccio può essere quello di tenere conto della posizione di
F# all'interno del framework .NET e progettare dunque librerie, cosiddette
vanilla, completamente aderenti alle
.NET Library Design Guidelines. In tal caso, occorre riservare i costrutti propri di F#, come le
discriminated union o i
function type, all'implementazione interna, evitandone l'uso nelle API pubbliche, e sottoporre le proprie interfacce all'esame di
FxCop. Un secondo approccio è, naturalmente, quello di applicare il paradigma funzionale anche alle metodologie di progettazione, utilizzando le proprietà di composizione del paradigma che portano a librerie in cui prima vengono definiti i tipi, e poi le funzioni che operano su di essi. Il testo riprende infine la creazione di librerie
.NET-compliant, fornendo una lunga serie di raccomandazioni.