Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Introduzione alla programmazione funzionale per gli sviluppatori .NET
Chris Marinos
Ormai è probabile che il nuovo linguaggio F #, l'ultimo nato della famiglia di linguaggi di Microsoft Visual Studio, non sia più completamente sconosciuto. Sono molti i motivi per cui avvicinarsi a F#: la sua sintassi pulita, le sue potenti funzionalità di multithreading e la sua interoperabilità fluida con gli altri linguaggi di Microsoft .NET Framework. Per utilizzare al meglio tutte queste funzionalità, tuttavia, sarà necessario acquisire familiarità con alcuni importanti concetti di base.
Una breve panoramica è un ottimo modo per accostarsi a un altro linguaggio orientato a oggetti o a un linguaggio dinamico come Ruby o Python. Questo perché gran parte del vocabolario è già noto e non rimane che apprendere la nuova sintassi. F# è diverso dagli altri. Trattandosi di un linguaggio di programmazione funzionale, il nuovo vocabolario che lo accompagna è sicuramente più esteso rispetto a quanto si sarebbe potuto prevedere. E se si considera che i linguaggi funzionali sono stati utilizzati soprattutto negli ambienti accademici, è intuibile come le definizioni dei nuovi termini possano essere di difficile comprensione.
Fortunatamente, F# non è stato progettato come un linguaggio accademico. La sua sintassi consente di utilizzare le tecniche funzionali per la risoluzione dei problemi continuando a supportare gli stili imperativi e orientati all'oggetto a cui uno sviluppatore .NET è sicuramente abituato. A differenza di altri linguaggi .NET, la sua struttura multi-paradigma consente di scegliere lo stile di programmazione migliore in base al problema da risolvere. La programmazione funzionale di F# verte sulla scrittura di codice conciso e funzionale per la risoluzione di problemi software pratici, nonché sull'utilizzo di tecniche quali funzioni di ordine superiore e composizione di funzioni per creare comportamenti di facile comprensione. Verte infine sulla compilazione di codice facile da interpretare, testare e parallelizzare rimuovendo le complessità nascoste.
Tuttavia, per poter usufruire al meglio di tutte le funzionalità di F#, è necessario assimilare alcuni concetti di base. In questo articolo verranno illustrati questi concetti utilizzando il vocabolario già noto a uno sviluppatore .NET. Verranno inoltre descritte alcune tecniche di programmazione funzionale che è possibile applicare al codice esistente e alcune operazioni di programmazione funzionale che si stanno già eseguendo. Leggendo l'intero articolo si saprà quanto basta sulla programmazione funzionale per poter utilizzare il linguaggio F# in Visual Studio 2010.
Concetti di base della programmazione funzionale
Per la maggior parte degli sviluppatori .NET, sarà più facile capire cos'è la programmazione funzionale una volta capito cosa non è. La programmazione imperativa è uno stile di programmazione considerato l'opposto della programmazione funzionale. È comunque lo stile di programmazione con cui si ha probabilmente maggiore familiarità perché la maggior parte dei principali linguaggi di programmazione è imperativa.
Le differenze tra programmazione funzionale e programmazione imperativa riguardano un livello fondamentale e sono percepibili anche nel codice più semplice:
int number = 0;
number++;
In questo codice si incrementa una variabile di uno. Nulla di interessante, ma si pensi a un modo diverso di risolvere il problema:
const int number = 0;
const int result = number + 1;
Il numero viene ancora incrementato di uno, ma non viene modificato sul posto. Il risultato viene invece archiviato come un'altra costante perché il compilatore non consente di modificare il valore di una costante. Si potrebbe dire che le costanti non sono modificabili perché non è possibile modificarne i valori dopo averli definiti. Viceversa, la variabile del numero del primo esempio è modificabile perché è possibile modificarne il valore. Questi due approcci illustrano una delle differenze fondamentali tra programmazione imperativa e programmazione funzionale. La programmazione imperativa si basa sull'utilizzo di variabili modificabili, mentre la programmazione funzionale utilizza valori non modificabili.
La maggior parte degli sviluppatori .NET direbbe che il numero e il risultato dell'esempio precedente sono variabili, mentre un programmatore funzionale sarebbe più cauto. Dopo tutto, l'idea di una variabile costante genera la massima confusione. I programmatori funzionali direbbero invece che il numero e il risultato sono valori. Assicurarsi di riservare il termine variabile agli oggetti modificabili. Si noti che questi termini non sono esclusivi della programmazione imperativa, ma sono ancora più importanti per la programmazione funzionale.
Questa distinzione potrebbe sembrare irrilevante, ma è la base di molti concetti che rendono così potente la programmazione funzionale. Le variabili modificabili sono la causa principale di molti bug. Come si vedrà in seguito, causano dipendenze implicite tra parti diverse del codice e hanno come conseguenza molti problemi, soprattutto correlati alla concorrenza. Al contrario, le variabili non modificabili comportano un livello di complessità inferiore. Conducono all'impiego di tecniche funzionali quali l'utilizzo delle funzioni come valori e la programmazione composizionale che verrà esaminata in seguito.
È naturale se in questa fase la programmazione funzionale lascia ancora spazio allo scetticismo. La maggior parte dei programmatori imperativi è portata a credere per formazione di non poter far nulla di utile con i valori non modificabili. Si consideri, tuttavia, l'esempio seguente:
string stringValue = "world!";
string result = stringValue.Insert(0, "hello ");
La funzione Insert ha creato la stringa "hello world!", ma si sa che Insert non modifica il valore della stringa di origine perché le stringhe non sono modificabili in .NET. I progettisti di .NET Framework hanno seguito un approccio funzionale perché risultava più semplice scrivere codice migliore con le stringhe. Poiché le stringhe sono tra i tipi di dati maggiormente utilizzati in .NET Framework (insieme ad altri tipi di base quali Integer, DateTimes e così via), è molto probabile che la programmazione funzionale eseguita risulti più utile del previsto.
Utilizzo di F#
F #viene fornito con Visual Studio 2010 e la versione più recente è scaricabile all'indirizzo msdn.microsoft.com/vstudio. Se si utilizza Visual Studio 2008, è possibile scaricare un componente aggiuntivo F# dal Centro per sviluppatori di F# all'indirizzo msdn.microsoft.com/fsharp, dove sono anche disponibili istruzioni relative all'installazione di Mono.
F# aggiunge a Visual Studio una nuova finestra denominata F# Interactive che consente di eseguire codice F# in modo interattivo. Si pensi a una versione più avanzata della finestra di controllo immediato, accessibile anche quando non è attiva la modalità di debug. Se si conosce Ruby o Python, si capirà che F# Interactive è Read-Evaluate-Print Loop (REPL), uno strumento utile ai fini dell'apprendimento di F# e delle sue tecniche di scrittura di codice.
In questo articolo F# Interactive verrà utilizzato per illustrare la compilazione e l'esecuzione di codice di esempio. Se si evidenzia codice in Visual Studio e si preme ALT+INVIO, si invia il codice a F# Interactive. Di seguito è riportato il semplice esempio di aggiunta in F#:
let number = 0
let result = number + 1
Quando si esegue il codice in F# Interactive, si ottiene quanto segue:
val number : int = 0
val result : int = 1
È probabile desumere dal termine val che numero e risultato sono entrambi valori non modificabili, non variabili modificabili. A questo scopo, utilizzare <-, l'operatore di assegnazione di F#:
> number <- 15;;
number <- 15;;
^^^^^^^^^^^^
stdin(3,1): error FS0027: This value is not mutable
>
Poiché è noto che la programmazione funzionale è basata sulla non modificabilità, questo errore dovrebbe avere un senso. La parola chiave let viene utilizzata per creare associazioni non modificabili tra nomi e valori. Nei termini di C#, in F# tutto è const per impostazione predefinita. È possibile creare una variabile modificabile se lo si desidera, ma lo si deve specificare in modo esplicito. Le impostazioni predefinite sono l'esatto contrario di ciò con cui si è familiari nei linguaggi imperativi:
let mutable myVariable = 0
myVariable <- 15
Inferenza dei tipi e riconoscimento degli spazi vuoti
F# consente di dichiarare variabili e valori senza specificarne il tipo, per questo si potrebbe desumere, sbagliando, che F# sia un linguaggio dinamico. F# è un linguaggio statico proprio come C# o C++. Tuttavia, il potente sistema di inferenza dei tipi di F# fa sì che lo sviluppatore possa evitare di specificare i tipi di oggetti in più posizioni. Ne consegue una sintassi semplice e concisa in grado di garantire l'indipendenza dai tipi dei linguaggi statici.
Sebbene sistemi di inferenza dei tipi come questo non siano realmente riscontrabili nei linguaggi imperativi, l'inferenza dei tipi non è direttamente correlata alla programmazione funzionale. L'inferenza dei tipi rimane comunque un concetto fondamentale ai fini dell'apprendimento del linguaggio F#. Gli sviluppatori C# conosceranno già con ogni probabilità l'inferenza dei tipi di base grazie alla parola chiave var:
// Here, the type is explictily given
Dictionary<string, string> dictionary =
new Dictionary<string, string>();
// but here, the type is inferred
var dictionary = new Dictionary<string, string>();
Entrambe le righe di codice C# creano nuove variabili tipizzate staticamente come Dictionary<string, string>, ma la parola chiave var indica al compilatore di dedurre il tipo della variabile. F# eleva questo concetto a un livello successivo. Di seguito è riportata una funzione add di esempio in F#:
let add x y =
x + y
let four = add 2 2
Nel codice non è presente una singola annotazione del tipo, tuttavia F# Interactive rivela la tipizzazione statica:
val add : int -> int -> int
val four : int = 4
Le frecce verranno spiegate in dettaglio in seguito, ma per il momento basti sapere che la funzione add viene definita per accettare due argomenti int e che four è un valore int. Il compilatore F# è giunto a questa deduzione in base al modo in cui add e four sono stati definiti. Le regole utilizzate dal compilatore per questo passaggio esulano dall'ambito di questo articolo, ma è possibile ottenere ulteriori informazioni visitando il Centro per sviluppatori di F#.
L'inferenza dei tipi viene utilizzata da F# per ottimizzare il codice, ma si noti l'assenza di parentesi graffe o parole chiave per indicare il corpo o il valore restituito della funzione add. Il motivo risiede nel fatto che il linguaggio F# è in grado di riconoscere gli spazi vuoti per impostazione predefinita. In F# è possibile identificare il corpo di una funzione tramite rientro, nonché restituire un valore assicurandosi che sia l'ultima riga nella funzione. Come l'inferenza dei tipi, il riconoscimento degli spazi vuoti non ha alcuna relazione diretta con la programmazione funzionale, ma è necessario conoscerla per poter utilizzare F#.
Effetti collaterali
Finora è stato spiegato che la programmazione funzionale è diversa dalla programmazione imperativa perché si basa su valori non modificabili anziché su variabili modificabili, ma questo aspetto non è di per sé molto utile. Il passaggio successivo consiste nel comprendere gli effetti collaterali.
Nella programmazione imperativa l'output di una funzione dipende dal relativo argomento di input e dallo stato corrente del programma. Nella programmazione funzionale le funzioni dipendono unicamente dai relativi argomenti di input. In altre parole, quando si chiama una funzione più di una volta con lo stesso valore di input, si ottiene sempre lo stesso valore di output. Il motivo per cui questo non accade nella programmazione imperativa è legato agli effetti collaterali, come illustrato nella Figura 1.
Figura 1 Effetti collaterali delle variabili modificabili
public MemoryStream GetStream() {
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.WriteLine("line one");
writer.WriteLine("line two");
writer.WriteLine("line three");
writer.Flush();
stream.Position = 0;
return stream;
}
[TestMethod]
public void CausingASideEffect() {
using (var reader = new StreamReader(GetStream())) {
var line1 = reader.ReadLine();
var line2 = reader.ReadLine();
Assert.AreNotEqual(line1, line2);
}
}
Alla prima chiamata a ReadLine, il flusso viene letto finché non viene incontrata una nuova riga. ReadLine restituisce quindi tutto il testo fino alla nuova riga. Tra questi passaggi, una variabile modificabile che rappresenta la posizione del flusso viene aggiornata. Ecco l'effetto collaterale. Alla seconda chiamata a ReadLine, il valore della variabile di posizione modificabile è cambiato, pertanto ReadLine restituisce un valore diverso.
Verrà ora analizzata una delle conseguenze più significative dell'utilizzo di effetti collaterali. Si consideri innanzitutto una classe PiggyBank semplice e alcuni metodi per utilizzarla (Figura 2).
Figura 2 Oggetti PiggyBank modificabili
public class PiggyBank{
public PiggyBank(int coins){
Coins = coins;
}
public int Coins { get; set; }
}
private void DepositCoins(PiggyBank piggyBank){
piggyBank.Coins += 10;
}
private void BuyCandy(PiggyBank piggyBank){
if (piggyBank.Coins < 7)
throw new ArgumentException(
"Not enough money for candy!", "piggyBank");
piggyBank.Coins -= 7;
}
Se si dispone di un salvadanaio contenente 5 monete, è possibile chiamare DepositCoins prima di BuyCandy, ma l'inversione dell'ordine comporta la generazione di un'eccezione:
// this works fine
var piggyBank = new PiggyBank(5);
DepositCoins(piggyBank);
BuyCandy(piggyBank);
// but this raises an ArgumentException
var piggyBank = new PiggyBank(5);
BuyCandy(piggyBank);
DepositCoins(piggyBank);
La funzione BuyCandy e la funzione DepositCoins aggiornano entrambe lo stato del salvadanaio tramite l'utilizzo di un effetto collaterale. Di conseguenza, il comportamento di ogni funzione dipende dallo stato del salvadanaio. Poiché il numero di monete è modificabile, l'ordine di esecuzione di queste funzioni è significativo. In altre parole, esiste una dipendenza temporale implicita tra questi due metodi.
Si renda ora di sola lettura il numero di monete per simulare una struttura di dati non modificabile. Nella Figura 3 viene illustrato che BuyCandy e DepositCoins restituiscono ora nuovi oggetti PiggyBank anziché aggiornare un oggetto PiggyBank esistente.
Figura 3 Oggetti PiggyBank non modificabili
public class PiggyBank{
public PiggyBank(int coins){
Coins = coins;
}
public int Coins { get; private set; }
}
private PiggyBank DepositCoins(PiggyBank piggyBank){
return new PiggyBank(piggyBank.Coins + 10);
}
private PiggyBank BuyCandy(PiggyBank piggyBank){
if (piggyBank.Coins < 7)
throw new ArgumentException(
"Not enough money for candy!", "piggyBank");
return new PiggyBank(piggyBank.Coins - 7);
}
Come prima, se si tenta di chiamare BuyCandy prima di DepositCoins, verrà generata un'eccezione di argomento:
// still raises an ArgumentException
var piggyBank = new PiggyBank(5);
BuyCandy(piggyBank);
DepositCoins(piggyBank);
Ora, invece, anche se si inverte l'ordine, si otterrà lo stesso risultato:
// now this raises an ArgumentException, too!
var piggyBank = new PiggyBank(5);
DepositCoins(piggyBank);
BuyCandy(piggyBank);
BuyCandy e DepositCoins dipendono unicamente dal relativo argomento di input perché il numero di monete non è modificabile. È possibile eseguire le funzioni in qualsiasi ordine e il risultato non cambia. La dipendenza temporale implicita è scomparsa. Tuttavia, poiché si desidera che BuyCandy abbia esito positivo, è necessario fare in modo che il risultato di BuyCandy dipenda dall'output di DepositCoins. È necessario rendere esplicita la dipendenza:
var piggyBank = new PiggyBank(5);
BuyCandy(DepositCoins(piggyBank));
Si tratta di una sottile differenza con pesanti conseguenze. Le dipendenze implicite e lo stato modificabile condiviso sono l'origine di alcuni dei bug più complessi che possono verificarsi nel codice imperativo e sono il motivo per cui il multithreading è così difficile in questo tipo di linguaggio. Dovendosi preoccupare dell'ordine di esecuzione delle funzioni, è necessario ricorrere a complicati meccanismi di blocco per evitare complicazioni. I programmi funzionali puri sono privi di effetti collaterali e di dipendenze temporali implicite, pertanto l'ordine di esecuzione delle funzioni è irrilevante. Ciò significa che non è necessario preoccuparsi di eventuali meccanismi di blocco e di altre tecniche di multithreading suscettibili di errore.
La semplificazione del multithreading è un motivo sostanziale della crescente attenzione che sta ottenendo la programmazione funzionale, ma non l'unico. Le funzioni prive di effetti collaterali sono più facilmente testabili perché ogni funzione si basa unicamente sui relativi argomenti di input. Sono più facili da gestire perché non si basano in modo implicito sulla logica di altre funzioni di impostazione. Le funzioni prive di effetti collaterali tendono inoltre a essere più piccole e più facilmente combinabili. Quest'ultimo punto verrà analizzato a breve in maggior dettaglio.
In F# le funzioni vengono valutate per i rispettivi valori restituiti e non per gli effetti collaterali che comportano. Nei linguaggi imperativi è comune chiamare una funzione per eseguire una qualche operazione. Nei linguaggi funzionali, le funzioni vengono chiamate per ottenere un risultato. A tale scopo, si osservi l'istruzione if in F#:
let isEven x =
if x % 2 = 0 then
"yes"
else
"no"
In F# l'ultima riga di una funzione è data dal relativo valore restituito, ma in questo esempio l'ultima riga della funzione è data dall'istruzione if. Non si tratta di un errore del compilatore. In F# anche le istruzioni if sono progettate per restituire valori:
let isEven2 x =
let result =
if x % 2 = 0 then
"yes"
else
"no"
result
Il valore restituito è di tipo string ed è assegnato direttamente all'istruzione if. Ricorda il funzionamento dell'operatore condizionale in C#:
string result = x % 2 == 0 ? "yes" : "no";
L'operatore condizionale preferisce restituire un valore anziché causare un effetto collaterale. Si tratta di un approccio più funzionale. Al contrario, l'istruzione if di C# è più imperativa perché non restituisce un risultato. Si limita a causare effetti collaterali.
Composizione di funzioni
Dopo avere approfondito i vantaggi derivanti dall'utilizzo delle funzioni prive di effetti collaterali, si sarà in grado di utilizzare le funzioni in F# sfruttandone l'intero potenziale. Si analizzi innanzitutto il codice C# necessario per calcolare la radice quadrata dei numeri da zero a dieci:
IList<int> values = 0.Through(10).ToList();
IList<int> squaredValues = new List<int>();
for (int i = 0; i < values.Count; i++) {
squaredValues.Add(Square(values[i]));
}
Fatta eccezione per i metodi di supporto Through e Square, questo codice è linguaggio C# standard. Gli sviluppatori C# più esperti si sentirebbero probabilmente offesi nel vedere l'utilizzo di un ciclo for anziché foreach. I linguaggi moderni come C# offrono i cicli foreach come un'astrazione per semplificare l'esame delle enumerazioni evitando la necessità di ricorrere a indicizzatori espliciti. L'obiettivo è stato raggiunto, ma si consideri il codice nella Figura 4.
Figura 4 Utilizzo dei cicli foreach
IList<int> values = 0.Through(10).ToList();
// square a list
IList<int> squaredValues = new List<int>();
foreach (int value in values) {
squaredValues.Add(Square(value));
}
// filter out the even values in a list
IList<int> evens = new List<int>();
foreach(int value in values) {
if (IsEven(value)) {
evens.Add(value);
}
}
// take the square of the even values
IList<int> results = new List<int>();
foreach (int value in values) {
if (IsEven(value)) {
results.Add(Square(value));
}
}
In questo esempio i cicli foreach sono simili, ma ogni corpo del ciclo esegue un'operazione leggermente diversa. I programmatori imperativi hanno sempre accettato questa duplicazione del codice perché quest'ultimo è considerato idiomatico.
I programmatori funzionali seguono invece un approccio diverso. Anziché creare astrazioni come cicli foreach per l'esame di elenchi, utilizzano funzioni prive di effetti collaterali:
let numbers = {0..10}
let squaredValues = Seq.map Square numbers
Anche questo codice F# calcola la radice quadrata di una sequenza di numeri, ma lo fa utilizzando una funzione di ordine superiore. Le funzioni di ordine superiore sono semplicemente funzioni che accettano un'altra funzione come argomento di input. In questo caso, la funzione Seq.map accetta la funzione Square come argomento. Applica questa funzione a ogni numero della sequenza e restituisce la sequenza di numeri al quadrato. Le funzioni di ordine superiore sono il motivo per cui molti affermano che la programmazione funzionale utilizza le funzioni come dati. Ciò significa che le funzioni possono essere utilizzate come parametri o assegnate a un valore o una variabile proprio come int o string. Nei termini di C#, ricorda i concetti di delegato ed espressione lambda.
Le funzioni di ordine superiore sono una delle tecniche che rendono così potente la programmazione funzionale. È possibile utilizzare le funzioni di ordine superiore per isolare il codice duplicato all'interno di cicli foreach e incapsularlo in funzioni autonome e prive di effetti collaterali. Queste funzioni eseguono ognuna una piccola operazione che il codice interno a un ciclo foreach avrebbe gestito. Poiché non implicano effetti collaterali, è possibile combinare queste funzioni per creare codice più leggibile e di più facile gestione in grado di eseguire le stesse operazioni dei cicli foreach:
let squareOfEvens =
numbers
|> Seq.filter IsEven
|> Seq.map Square
L'unico aspetto di questo codice che potrebbe generare confusione è dato dall'operatore |>. Questo operatore viene utilizzato per rendere il codice ancora più leggibile consentendo il riordino degli argomenti a una funzione in modo che l'ultimo argomento venga letto per primo. La sua definizione è molto semplice:
let (|>) x f = f x
Senza l'operatore |>, il codice squareOfEvens sarebbe simile al seguente:
let squareOfEvens2 =
Seq.map Square (Seq.filter IsEven numbers)
Se si utilizza LINQ, questa modalità di utilizzo delle funzioni di ordine superiore dovrebbe risultare familiare. Questo perché LINQ è profondamente radicato nella programmazione funzionale. Di fatto, è facilmente possibile convertire il problema SquareOfEvens in C# utilizzando metodi da LINQ:
var squareOfEvens =
numbers
.Where(IsEven)
.Select(Square);
Ne deriva la sintassi di query LINQ seguente:
var squareOfEvens = from number in numbers
where IsEven(number)
select Square(number);
Utilizzando LINQ nel codice C# o Visual Basic è possibile sfruttare al meglio le potenzialità della programmazione funzionale. È un ottimo modo per apprendere le tecniche di programmazione funzionale.
Man mano che si inizierà a utilizzare regolarmente le funzioni di ordine superiore, prima o poi ci si imbatterà in una situazione nella quale si dovrà creare una funzione piccola e molto specifica da passare in una funzione di ordine superiore. I programmatori funzionali utilizzano le funzioni lambda per risolvere questo problema. Le funzioni lambda sono semplicemente funzioni la cui definizione non prevede l'assegnazione di un nome. Sono generalmente piccole e presentano un utilizzo molto specifico. Di seguito viene illustrato un altro modo per calcolare la radice quadrata di numeri pari tramite una funzione lambda:
let withLambdas =
numbers
|> Seq.filter (fun x -> x % 2 = 0)
|> Seq.map (fun x -> x * x)
L'unica differenza tra questo e il codice precedente è che Square e IsEven sono definiti come funzioni lambda. In F # una funzione lambda viene dichiarata utilizzando la parola chiave fun. Utilizzare le funzioni lambda solo per dichiarare funzioni con singolo utilizzo perché non possono essere facilmente utilizzate nell'ambito in cui sono state definite. Per questo motivo, Square e IsEven non sono candidati ideali per funzioni lambda perché sono utili in molte situazioni.
Currying e applicazione parziale
Finora in questo articolo sono stati trattati tutti i concetti di base necessari per apprendere l'utilizzo di F#, tranne uno altrettanto importante. Negli esempi precedenti, l'operatore |> e le frecce nelle firme dei tipi da F# Interactive sono entrambi correlati a un concetto noto come currying.
Con il termine currying si intende la scomposizione di una funzione con molti argomenti in una serie di funzioni che accettano ognuna un argomento e infine producono lo stesso risultato della funzione originale. Il currying è probabilmente l'argomento più complesso affrontato in questo articolo per uno sviluppatore .NET, soprattutto perché viene spesso confuso con il concetto di applicazione parziale. Nell'esempio seguente verrà illustrato il funzionamento di entrambi:
let multiply x y =
x * y
let double = multiply 2
let ten = double 5
Il comportamento diverso rispetto alla maggior parte dei linguaggi imperativi dovrebbe essere abbastanza evidente. La seconda istruzione crea una nuova funzione denominata double passando un argomento a una funzione che accetta due. Il risultato è una funzione che accetta un argomento int e produce lo stesso output come se si fosse chiamato multiply con x uguale a 2 e y uguale a quell'argomento. In termini di comportamento, corrisponde al codice seguente:
let double2 z = multiply 2 z
Spesso si afferma erroneamente che multiply viene sottoposto a currying per formare double. Ma questo non è del tutto vero. La funzione multiply viene sottoposta a currying, ma questo si verifica quando viene definita perché le funzioni in F# sono sottoposte a currying per impostazione predefinita. Quando viene creata la funzione double, è più preciso dire che la funzione multiply viene parzialmente applicata.
Scendendo nei dettagli, si può affermare che il currying scompone una funzione con molti argomenti in una serie di funzioni che accettano ognuna un argomento e infine producono lo stesso risultato della funzione originale. La funzione multiply presenta la firma del tipo seguente in base a F# Interactive:
val multiply : int -> int -> int
Fino a questo punto, si è inteso che multiply è una funzione che accetta due argomenti int e restituisce un risultato int. Ora verrà illustrato ciò che realmente accade. La funzione multiply è veramente una serie di due funzioni. La prima funzione accetta un argomento int e restituisce un'altra funzione, associando efficacemente x a un valore specifico. Questa funzione accetta inoltre un argomento int a cui è possibile pensare come il valore da associare a y. Dopo avere chiamato questa seconda funzione, x e y sono entrambi associati, pertanto il risultato è il prodotto di x e y come definiti nel corpo di double.
Per creare double, la prima funzione nella catena di funzioni multiply viene valutata per l'applicazione parziale di multiply. Alla funzione risultante verrà assegnato il nome double. Quando viene valutata la funzione double, il relativo argomento viene utilizzato insieme al valore parzialmente applicato per creare il risultato.
Utilizzo di F# e della programmazione funzionale
Avendo acquisito sufficiente vocabolario per apprendere l'utilizzo di F# e della programmazione funzionale, si può decidere di procedere in molti modi.
F# Interactive consente di esplorare il codice F# e di compilare rapidamente script F#. È inoltre utile per fornire risposte a domande comuni circa il comportamento delle funzioni della libreria .NET senza ricorrere a file della Guida o a ricerche sul Web.
F# non ha rivali per quanto riguarda l'espressione di algoritmi complessi, pertanto è possibile incapsulare queste parti delle applicazioni nelle librerie F# che possono essere chiamate da altri linguaggi .NET, particolarmente utile nella applicazioni di progettazione o in situazioni multithread.
Infine, è possibile utilizzare le tecniche di programmazione funzionale nello sviluppo .NET quotidiano senza scrivere codice F#. Utilizzare LINQ anziché cicli for o foreach. Provare a utilizzare i delegati per creare funzioni di ordine superiore. Limitare l'utilizzo della modificabilità e degli effetti collaterali nella programmazione imperativa. Se si inizia a scrivere codice in stile funzionale, si avrà sicuramente il desiderio di scrivere più codice F#.
Chris Marinos è un consulente software in SRT Solutions in Ann Arbor, Mich. È possibile ascoltare i suoi interventi su F#, sulla programmazione funzionale e su altri interessanti argomenti in occasione di eventi nella zona di Ann Arbor o sul suo blog all'indirizzo srtsolutions.com/blogs/chrismarinos.
Un ringraziamento ai seguenti esperti tecnici per la revisione dell'articolo: Luke Hoban