Per gli estimatori di Tolkien, il titolo del post può sembrare presuntuoso ma in fin dei conti ho viaggiato per 7.600 km, molti più di Bilbo e compagnia, ho incontrato persone delle quali non capivo la lingua, più o meno, e ho parlato con un drago… Ok, questo non l’ho fatto, ma lasciatemelo credere 😃
Scherzi a parte, ho passato i primi sei mesi di quest’anno ad Indianapolis negli Stati Uniti. In questo periodo ho avuto la fortuna di toccare con mano la vivacità della comunità tecnologica dell’Indiana partecipando a due conferenze, Indy.Code e AgileIndy conference, e a svariati meetup: Agileindy, Indy Software Artisans, Agile Games, Indy .NET Consortium, etc.
Uno degli argomenti che più ha attirato la mia curiosità è stata la programmazione funzionale. Il motivo di questo interesse è stato triplice: l’approccio per me nuovo; le promesse di questo paradigma; e la passione trasmessa da Dave Fancher sull’argomento.
Uno delle caratteristiche più interessanti della programmazione funzionale è sicuramente l’immutabilità delle “variabili” (anche se sarebbe più corretto chiamarle “immutabili” 😃) e come essa porta a strutturare diversamente il codice dell’applicazione rispetto alla programmazione orientata agli oggetti (OOP).
Per evidenziare le differenze dei due approcci, su questo specifico aspetto, ho implementato una stessa applicazione d’esempio due volte, utilizzando sia C# (OOP) che F# (programmazione funzionale). L’applicazione deve consentire di:
- selezionare un conto corrente fornendo il nome dell’intestatario, e leggendo i dati corrispondenti da DB;
- richiedere il prelievo di una data somma di denaro dal conto corrente selezionato visualizzando il nuovo saldo;
- chiedere conferma all’utente, e salvare il nuovo saldo sul DB in caso di risposta affermativa;
- reiterare le operazioni sopra fino a quando l’utente non decide di uscire.
Il codice completo degli esempi sotto è disponibile su GitHub.
C#
Per prima cosa ho definito il modello del conto corrente.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BankAccount
{
public BankAccount()
{
}
public BankAccount(string owner, decimal balance)
{
Owner = owner;
Balance = balance;
}
public int Id { get; private set; }
public string Owner { get; private set; }
public decimal Balance { get; private set; }
public void Withdraw(decimal amount)
{
Balance -= amount;
}
}
Id
serve ad agevolare l’utilizzo di Entity
Framework per la persistenza dei dati su DB. Tralascerò tutto il codice relativo
ad EF perché di scarso interesse ai fini di questo post.
Dopodiché ho creato un applicazione console per soddisfare i requisiti di cui sopra.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using System;
using System.Linq;
class Program
{
static void Main(string[] args)
{
using (BankAccountContext context = new BankAccountContext())
{
while (TryWithdrawFromAccount(context)) ;
}
}
private static bool TryWithdrawFromAccount(BankAccountContext context)
{
string owner;
BankAccount bankAccount;
Console.Write("Type owner (or 'q' to exit): ");
owner = Console.ReadLine();
if (owner == "q")
{
return false;
}
bankAccount = context.BanckAccounts.FirstOrDefault(ba => ba.Owner == owner);
if (bankAccount != null)
{
WithdrawFromAccount(context, bankAccount);
}
return true;
}
private static void WithdrawFromAccount(BankAccountContext context,
BankAccount bankAccount)
{
Console.WriteLine($"{bankAccount.Owner} has {bankAccount.Balance}$");
Console.Write("How much you want to withdraw? ");
bankAccount.Withdraw(decimal.Parse(Console.ReadLine()));
if (ConfirmWithdraw(bankAccount) == "y")
{
context.SaveChanges();
}
}
private static string ConfirmWithdraw(BankAccount bankAccount)
{
Console.Write($"{bankAccount.Owner} new balance will be " +
$"{bankAccount.Balance}. Confirm operation? [y/n] ");
return Console.ReadLine();
}
}
Main
viene creato il contesto di EF mentre tutta la logica è implementata
nei metodi TryWithdrawFromAccount
e WithdrawFromAccount
. I punti principali
dell’applicazione sono:
- il recupero del conto corrente alla riga 25;
- il prelievio della somma di denaro dal conto corrente alla riga 39;
- e la richiesta di conferma con salvataggio alle righe 41-44.
Tutto facile, tutto perfetto… o forse no? Eseguendo il programma tutto funziona correttamente nel caso di conferma del prelievo. Viceversa, se l’utente non conferma il prelievo, e richiede nuovamente il saldo dello stesso conto, il programma ha il comportamento mostrato qui sotto.
Ai più esperti sarà subito saltato all’occhio l’errore: anche se il salvataggio
del nuovo saldo è condizionato alla conferma dell’utente, riga 41, l’oggetto bankAccount
è già stato modificato e fa parte del contesto di EF, il quale viene disposto solo
alla chiusura dell’applicazione. Questo fa sì che la seconda volta che l’utente richiede
il saldo del conto di Alice il valore visualizzato non sia quello originale ma quello
modificato.
Qui sotto riporto i metodi modificati rispetto al programma precedente per ottenere
il comportamento desiderato.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private static bool TryWithdrawFromAccount(BankAccountContext context)
{
string owner;
BankAccount bankAccount;
Console.Write("Type owner (or 'q' to exit): ");
owner = Console.ReadLine();
if (owner == "q")
{
return false;
}
bankAccount = TryGetBankAccountByOwner(context, owner);
if (bankAccount != null)
{
WithdrawFromAccount(context, bankAccount);
}
return true;
}
private static void WithdrawFromAccount(BankAccountContext context,
BankAccount bankAccount)
{
Console.WriteLine($"{bankAccount.Owner} has {bankAccount.Balance}$");
Console.Write("How much you want to withdraw? ");
bankAccount.Withdraw(decimal.Parse(Console.ReadLine()));
if (ConfirmWithdraw(bankAccount) == "y")
{
UpdateBankAccount(context, bankAccount);
}
}
private static BankAccount TryGetBankAccountByOwner(BankAccountContext context,
string owner)
{
/*** Load detached object ***/
return context.BanckAccounts.AsNoTracking()
.FirstOrDefault(ba => ba.Owner == owner);
}
private static void UpdateBankAccount(BankAccountContext context,
BankAccount bankAccount)
{
/*** Attach the modified object to the context ***/
context.Entry(bankAccount).State = System.Data.Entity.EntityState.Modified;
context.SaveChanges();
}
AsNoTracking
a riga 38) e ricollegarlo alla sessione soltanto dopo la conferma da parte dell’utente
(riga 46).
Vediamo ora come è possibile risolvere lo stesso problema in F#.
F#
Anche in questo caso, ho definito immediatamente il modello di dominio. Esso consiste
di un record
e una funzione withdraw
la quale, seguendo il principio dell’immutabilità, crea
un nuovo record uguale a quello passato come parametro ma con il saldo aggiornato.
1
2
3
4
5
6
module BankAccount
type BankAccount = {Id:int; Owner:string; Balance:decimal}
let withdraw = fun bankAccount (amount:decimal) ->
({ bankAccount with Balance = bankAccount.Balance - amount })
Dopodiché ho definito le funzioni di accesso ai dati le quali, sfruttando la potenza
del Data Provider di F# per SQL Server, e lo stesso DB utilizzato per l’esempio C#,
mappano semplicemente il record BanckAccount
definito sopra con i campi del DB.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module DataAccess
open Microsoft.FSharp.Data.TypeProviders
open BankAccount
[<Literal>]
let connectionString = "Data Source=(localdb)\mssqllocaldb;Initial Catalog=BankAccounts;Integrated Security=True"
type dbSchema = SqlDataConnection<connectionString>
let tryGetBankAccountByOwner =
fun (db:dbSchema.ServiceTypes.SimpleDataContextTypes.BankAccounts) owner ->
(query { for ba in db.BankAccounts1 do
where (ba.Owner = owner);
select {
Id = ba.Id;
Owner = ba.Owner;
Balance = ba.Balance
}
} |> Seq.tryHead)
let updateBankAccount =
fun (db:dbSchema.ServiceTypes.SimpleDataContextTypes.BankAccounts) bankAccount ->
(query { for ba in db.BankAccounts1 do
where (ba.Id = bankAccount.Id);
select ba
} |> Seq.iter(fun ba ->
ba.Owner <- bankAccount.Owner;
ba.Balance <- bankAccount.Balance)
db.DataContext.SubmitChanges())
Infine ho creato un’applicazione console la quale assomiglia in tutto e per
tutto a quella C#.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module Program
open System
open BankAccount
open DataAccess
let printConfirmWithdraw = fun bankAccount ->
printf "%s new balance will be %M. Confirm operation? [y/n] "
bankAccount.Owner bankAccount.Balance
let confirmWithdraw =
fun bankAccount amount -> amount |> withdraw bankAccount |> printConfirmWithdraw
Console.ReadLine()
let withdrawFromAccount = fun db bankAccount ->
(printfn "%s has %M$" bankAccount.Owner bankAccount.Balance
printf "How much you want to withdraw? "
let amount = Console.ReadLine() |> decimal
match confirmWithdraw bankAccount amount with
| "y" -> amount |> withdraw bankAccount |> updateBankAccount db
| _ -> ())
let tryWithdrawFromAccount = fun db ->
printf "Type owner (or 'q' to exit): "
match Console.ReadLine() with
| "q" -> false
| owner -> (match tryGetBankAccountByOwner db owner with
| None -> ()
| Some bankAccount -> withdrawFromAccount db bankAccount
true)
[<EntryPoint>]
let main argv =
let db = dbSchema.GetDataContext()
while tryWithdrawFromAccount(db) do ()
0
main
che si occupa di recuperare
la connessione al DB (riga 34) e la logica di interazione con l’utente è implementata
nelle funzioni tryWithdrawFromAccount
e withdrawFromAccount
.
I punti principali dell’applicazione sono:
- il recupero del conto corrente alla riga 27;
- il prelievio della somma di denaro dal conto corrente alla riga 20;
- e la richiesta di conferma con salvataggio sempre alla riga 20.
In questo caso posso dire: tutto perfetto, tutto facile… un po’ meno per uno abituato da sempre alla programmazione ad oggetti.
Conclusioni
Sviluppando questa semplice applicazione d’esempio, l’immutabilità mi ha aiutato, in modo molto naturale, a definire chiaramente il confine fra lettura/scrittura dei dati e elaborazione degli stessi. Come ho cercato di evidenziare nell’esempio C#, anche utilizzando la OOP si può ottenere lo stesso grado di separazione.
La differenza fra i due approcci sta nel fatto che in OOP, per scrivere del buon codice, si deve essere molto disciplinati mentre in programmazione funzionale per riuscire a scrivere del cattivo codice si deve essere particolarmente indisciplinati 😃 D’altro canto la programmazione orientata agli oggetti è probabilmente più intuitiva rispetto a quella funzionale la quale, di base, è più formale.
Ci sono molte altre caratteristiche di cui si legge normalmente nei confronti fra programmazione funzionale e programmazione orientata gli oggetti: leggibilità del codice, prolissità dello stesso, performance, etc. Se ne avrò l’occasione farò qualche altro esperimento per capirci qualcosa di più e ne condividerò i risultati.