Guest post: F# vs C#
Questo articolo è stato scritto da Nicola Iarocci , sviluppatore, esperto di MongoDB e autore di alcuni progetti open source in Python e .NET
Secondo la definizione ufficiale F# è "un linguaggio che offre supporto per la programmazione funzionale in aggiunta a quella tradizionale, imperativa e procedurale, orientata agli oggetti."
Come incipit non è granché. Tante volte mi son detto che sì, un nuovo linguaggio Microsoft è interessante, ma in fin dei conti cosa aggiunge di nuovo? Supporta la piattaforma .NET e va bene, ma .NET già lo uso da C#. E poi, "funzionale"... suona come una roba da matematici e statistici. Io scrivo applicazioni business e lo faccio manipolando oggetti. Proprietà e metodi, non funzioni, sono il mio pane quotidiano. Meglio lasciare F# e le sue funzioni alle università, ai centri di ricerca o magari ai trader rampanti.
Ed è così che F# è rimasto per un bel po' nel cassetto delle cose interessanti da guardare prima o poi, senza fretta. Se più o meno vi ci ritrovate in queste considerazioni non preoccupatevi perché non siete i soli. In tempi più o meno recenti praticamente chiunque ho incontrato in giro per l'Italia mi ha risposto in questi termini ogni volta che la discussione dirottava in un modo o nell'altro su F# e sui linguaggi funzionali: "Bello sì, interessante. Prima o poi..."
Per me il "poi" è finalmente arrivato un paio d'anni fa quando ho ripreso in mano il linguaggio e mi sono messo a pastrocchiarci per davvero. Ho scoperto un modo nuovo di risolvere problemi e un linguaggio super interessante.
Perché F# è interessante
La prima cosa da sapere su F# è che è adattissimo a ogni tipo di applicazione, non solo quelle matematico-scientifiche. Una volta presa dimestichezza col linguaggio scopriamo che tutti i problemi che affrontiamo ogni giorno possono essere risolti brillantemente in F#; spesso in modo più succinto, leggibile e meno prono ad errori.
La seconda è che, pure essendo un linguaggio funzionale, F# ci consente di usare gli oggetti. Non potrebbe essere altrimenti visto che supporta il .NET Framework. Quello che segue è il classico Hello World implementato in Windows Forms con F#, ed è tratto dall'ottimo Real World Functional Programming di Tomas Petriceck (pag. 26):
open System.Drawing
open System.Windows.Forms
type HelloWindow() =
let frm = new Form(Width = 400, Height = 140)
let fnt = new Font("Times New Roman", 28.0f)
let lbl = new Label(Dock = DockStyle.Fill, Font = fnt,
TextAlign = ContentAlignment.MiddleCenter)
do frm.Controls.Add(lbl)
member x.SayHello(name) =
let msg = "Hello " + name + "!"
lblT.Text <- msg
member x.Run() =
Application.Run(frm)
Per lanciare l'applicazione:
let hello = new HelloWindows()
hello.SayHello("caro lettore")
hello.Run()
Familiare, vero? Il buon vecchio .NET Framework al lavoro. Il codice non è certo un esempio di purezza funzionale, ma porta a casa brillantemente il risultato.
La terza cosa da sapere su F# è che essendo anch'esso un linguaggio CLR offre interoperabilità completa con C# e VB. Così come è possibile avere soluzioni Visual Studio che combinano progetti C# e VB, lo stesso possiamo fare con F#. Questo ci offre l'opportunità di cominciare a inserire piccole componenti F# all'interno di soluzioni già mature, scritte in altri linguaggi. E' questo l'approccio che ho usato io non appena ho smesso di giocare con progetti di studio.
Immutabile e funzionale
Una caratteristica dei linguaggi funzionali che lascia sempre interdetti noi "oggettisti" di vecchia data è l'intrinseca immutabilità dei valori. Per incrementare un numero in C# faremmo qualcosa di questo genere:
var numero = 10;
numero++;
Dopo aver assegnato un valore iniziale a number
procediamo ad incrementarne il valore. number
dunque è mutabile, tant'è vero che lo definiamo una "variabile" (nomen omen). Se volessimo adottare un approccio funzionale allo stesso problema dovremmo andare in un'altra direzione, dato che non possiamo mutare nulla:
const int numero = 10;
const int risultato = numero + 1;
Intanto, numero
è una costante, non più una variabile, il che ha senso visto che non possiamo mutarne comunque il valore. Ne incrementiamo ancora il valore, ma il risultato dell'operazione lo assegniamo ad un'altra costante. Potrebbe sembrare che in questo modo stiamo usando più risorse e scrivendo più del necessario, ed in effetti in C# questo è probabilmente vero, ma quel che ci interessa è comprendere la differenza tra un approccio imperativo e mutabile ("cambia il valore di numero
, aumentandolo di una unità") e quello funzionale e immutabile ("risultato
è pari alla somma tra il valore di numero
e uno"). Visto un codice di questo tipo verrebbe subito voglia di creare una funzione che, dato un numero n
, restituisce il valore di n + 1
, vero? Non è un caso.
Sappiamo per certo che sia numero
che risultato
non cambieranno mai, il che ci tranquillizza nell'utilizzare queste costanti ovunque nel nostro programma. Vi è mai successo che il cambio di stato all'interno di un oggetto (alla cui implementazione magari non avete accesso) causasse conseguenze impreviste sul vostro codice? Un cosa del genere non capita mai in un linguaggio funzionale puro, semplicemente perché non esiste uno stato che possa mutare.
In effetti, i linguaggi funzionali classici non contemplano il concetto di "variabile" e tantomeno quello di "costante": in un linguaggio funzionale si maneggiano solo "valori" (e naturalmente le funzioni sono valori a loro volta).
Detto questo, e per le ragioni viste sopra (compatibilità con .NET e i suoi oggetti), F# è meno intransigente, e consente in realtà l'uso di valori mutabili:
let mutable numero = 10 numero <- 11
Potete farlo, ma dovete essere espliciti, con tanto di parola chiave mutable
ed operatore <-
dedicati. I valori, anche in F# come negli altri linguaggi funzionali, sono immutabili per default.
L'immutabilità non è un concetto alieno allo stesso C#. Le stringhe per esempio sono immutabili, il che spiega perché in C# i metodi di una stringa restituiscono nuove stringhe invece di aggiornarla.
Ci si abitua in fretta ad usare valori invece di variabili. E se all'inizio può sembrare una gran perdita di tempo ben presto ci si rende conto dei notevoli vantaggi che l'immutabilità comporta, soprattutto in termini di stabilità del codice.
Fortemente tipizzato, come fosse dinamico
F# è fortemente tipizzato come C#, ma adotta un sistema di inferenza dei tipi che lo fa sembrare in tutto e per tutto un linguaggio dinamico. In C# abbiamo imparato (io con qualche resistenza) ad usare var
per ottenere qualcosa di simile, ma F# sposta l'asticella a tutta un'altra altezza.
In effetti a parte rari casi non vi troverete mai a dichiarare un tipo in F#. Un esempio l'abbiamo già visto prima, vediamone un altro paio:
let x = 42
let b = false
Il valore a destra dell'operatore consente di indovinare facilmente il tipo di valore. Fin qui non c'è molto di nuovo: potremmo considerare let
l'equivalente di var
in C#. Considerate però l'esempio seguente:
let d = new Dictionary<int,string>()
for kvp in d do
printfn "%d: %s" kvp.Key kvp.Value
La seconda e terza riga sono interessanti, vero? L'inferenza dei tipi aiuta a rendere il codice estremamente compatto, il che, oltre a risparmiarci del lavoro, lo rende anche più leggibile.
Per una trattazione approfondita della inferenza dei tipi vi invito a leggere Overview of type inference in F#, da cui questi esempi sono tratti. Tra parentesi, se come il sottoscritto siete già abituati ad un linguaggio dinamico come Python, qui vi troverete a casa.
Una shell interattiva
Un'altra caratteristica interessante di F# che di nuovo ricorda i linguaggi dinamici è la sua shell interattiva (fsi.exe) che consente di sperimentare liberamente, anche man mano che si sta scrivendo il programma principale. In Visual Studio la attivate con Ctrl+Alt+F, oppure dal menu Viste. Potete anche lanciarla manualmente dal prompt dei comandi.
Inserite il codice come fareste nell'editor di testo, anche su più righe. Quando siete pronti a lanciarlo completate con una sequenza di due punti e virgola (;;
).
Approfitto della shell per proporre un esempio di codice interessante, anche questo preso dal libro di Petriceck (pag. 42), che ci consente di vedere rapidamente una serie di caratteristiche interessanti del linguaggio:
> let numbers = [1 .. 10]
let isOdd(n) = n % 2 = 1
let square(n) = n * n
;;
val numbers : int list
val isOdd : int -> bool
val square : int -> int
> List.filter isOdd numbers;;
val it : int list = [1; 3; 5; 7; 9]
> List.map square (List.filter isOdd numbers);;
val it : int list = [1; 9; 25; 49; 81]
Andiamo con ordine. Definiamo una lista numbers
che contiene i numeri da 1 a 10 (la sequenza è auto generata per noi), quindi due funzioni in linea: isOdd
ci dice se un valore è dispari e square
calcola il quadrato di un valore. Infine lanciamo la compilazione digitando i due punti e virgola. La shell ci risponde confermandoci che il sistema di inferenza dei tipi ha fatto il suo lavoro: numbers
è in effetti un valore, nello specifico una lista di interi; isOdd
una funziona che accetta un intero e restituisce un valore booleano; square
invece accetta e restituisce valori interi.
Nella seconda fase invochiamo una funzione di libreria, List.filter
, che accetta una funzione di convalida (isOdd
nel nostro caso) e una lista di valori (numbers
). I valori verranno passati uno alla volta alla funzione di convalida e, se convalidati, aggiunti a una nuova lista di valori in uscita. Poiché in questo caso diamo subito il comando di esecuzione (al solito, il doppio punto e virgola) possiamo apprezzare subito il risultato, ovvero la lista di numeri dispari contenuti in numbers
.
La terza fase è simile alla prima, solo che questa volta usiamo un'altra funzione predefinita, List.map
, la quale non fa altro che passare i valori in ingresso a una funzione che li manipola, nel nostro caso square
. Come potete vedere qui ci complichiamo la vita usando il risultato di List.filter
come serie di input. Il risultato è una lista con i quadrati dei numeri dispari in numbers
.
Notate anche come in F# non si usano parentesi per passare parametri alle funzioni, cosa che inizialmente può creare confusione all'occhio non allenato (nel caso di List.map
le parentesi sono state usate solo per indicare una precedenza). Questione di minuti, poi ci si chiede come possa essere altrimenti.
Niente parentesi e punti e virgola (yay Python!)
Come abbiamo già visto in F# è stato messo molto impegno nella semplificazione della sintassi: inferenza dei tipi, niente parentesi nei passaggi di parametri e, se non l'avete già notato, niente punti e virgola a chiudere le istruzioni; ma non finisce qui.
La novità senz'altro più interessante in questo ambito è l'adozione della indentazione a fini sintattici. Non è vera innovazione poiché diversi linguaggi (Python, Haskell, occam, ABC, ecc.) l'hanno adottata da tempo, e per un'ottima ragione: addio parentesi graffe!
let isOdd n =
let mod = n % 2
mod = 1
Tutto quel che è nidificato sotto la dichiarazione let
fa parte della funzione isOdd
. Notate anche che non c'è return
. Non è necessario, o meglio, è sottointeso: una funzione restituisce sempre il risultato dell'ultima espressione contenuta al suo interno.
Piping come fosse LINQ (o viceversa?)
A ben guardare, il processo di ibridazione tra OOP e programmazione funzionale è avviato da tempo e C# e VB stanno lì a dimostrarlo. Le espressioni lambda vengono dai linguaggi funzionali, e LINQ è in effetti una vera e propria “bomba funzionale” innestata di soppiatto nell’universo .NET.
In LINQ quel che facciamo continuamente è passare il risultato di una elaborazione (funzione) ad un'altra funzione. In C#:
var interi = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var pari = interi.Select(e => e * 2).ToArray();
// {0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
In F# otteniamo lo stesso con l'operatore di piping:
let interi = [1 .. 10]
let pari = interi |> List.map(fun(x) -> x * 2)
// [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Il valore a sinistra dell'operatore |>
viene passato in ingresso alla funzione a destra.
Quale è il vantaggio del piping? Prima di tutto l'inferenza dei tipi, di nuovo. F# supporta l'inferenza da sinistra a destra dell'operatore, il che vuol dire che non è necessario definire il tipo dei valori in entrata.
Nell'esempio qui sopra non si apprezza una grande differenza perché usiamo una funziona anonima, ma in LINQ alcuni metodi come GroupBy
richiedono che il tipo sia sempre conosciuto; ciò non è mai vero in F#.
let aumenta items = items |> Seq.map (fun i -> i + 1)
In LINQ (che naturalmente è supportato in F#) diventa:
let aumenta (items:seq<_>) = items.Select(fun x -> x + 1)
In cui dobbiamo definire il tipo di items
. Man mano che si prende confidenza con lo stile funzionale viene naturale rendere il codice sempre più conciso. Il codice in effetti può essere ridotto in:
let aumenta = Seq.map ((+) 1)
Aggiungo che mentre in C# il chaining dei metodi è possibile nel contesto di LINQ, il pipeline operator di F# è parte integrante del linguaggio e può essere (e di fatto viene) usato ovunque. Ecco un altro esempio che mette insieme molte delle cose viste finora:
let sommaDeiQuadrati = [1..10] |> List.map square |> List.sum
Non si può certo dire che F# non sia un linguaggio conciso.
Altre cose belle e un consiglio
Ci sono molte cose eccitanti da scoprire in F#: pattern matching, unit of measure (queste le adoro), discriminated unions, type providers... ma io mi fermo qui.
Vi consiglio di non perdervi il famoso talk An Introduction to Microsoft F# di Luca Bolognese. Se non ci sono riuscito io, cosa più che probabile, lui senz'altro saprà convincervi a rispolverare quella vecchia idea di dare un'occhiata a F#. Meglio prima che poi.
Comments
Anonymous
April 16, 2015
Articolo interessante, specialmente per chi (come me) conosce C# dalla prima versione e vuole imparare F# Qualche settimana parlavamo proprio di questo argomento nel gruppo dedicato ai linguaggi di programmazione per .NET Framework nella Community Yammer dei Microsoft MVP.Anonymous
April 16, 2015
Mi è capitato di sentire un paio di settimane fa la filippica (annotata) di un fan di LISP e funzionali. Torno a casa e comincio a guardare F#. Poi leggo qua. Per il momento non posso fare a meno di ripetere: e quindi? Devo ancora vedere un ESEMPIO dove si vede la potenza del funzionale. Non metto minimamente in dubbio che esistano, solo che io non ho ancora avuto modo di vederli. Quanto all'esempio WinForms ... è ovvio che F# è più compatto, ma HelloWorld "classico" è a costo zero perché fa tutto VS. E quindi? :)