Funzioni Pure e Impure ed Effetti Collaterali in Linguaggio C
Una funzione può avere variabili locali su cui operare che sono visibili solo al proprio interno. Tuttavia, è anche possibile che una funzione in C possa modificare variabili globali. Questo è un esempio di effetto collaterale.
Dal momento che una chiamata a funzione possa essere usata all'interno di un'espressione, questo concetto ha delle ripercussioni importanti sul risultato finale.
Per questo motivo, in questa lezione introdurremo alcuni concetti fondamentali: le funzioni pure e le funzioni impure. Vedremo cosa sono gli Effetti Collaterali e come questi possano influenzare il risultato finale di un'espressione.
Il concetto di Funzione Pura e Funzione Impura
Nelle precedenti lezioni abbiamo studiato le funzioni ed i meccanismi per definirle e chiamarle. Abbiamo, inoltre, approfondito i concetti di variabili locali, variabili statiche e variabili globali; in altre parole lo scope delle variabili.
Con questi concetti chiari in mente, possiamo introdurre un fondamentale concetto, a prima vista solo teorico, che ha delle ripercussioni importanti sullo sviluppo di codice C: le funzioni pure e le funzioni impure.
Una funzione in linguaggio C si dice pura se rispetta due condizioni:
- A parità di argomenti passati come parametri restituisce sempre lo stesso risultato;
- Non modifica lo stato globale del programma.
Analizziamo singolarmente queste due proprietà.
Partiamo da un primo esempio di funzione pura: una funzione che calcola la lunghezza dell'ipotenusa di un triangolo rettangolo data la lunghezza dei due cateti.
#include <math.h>
double ipotenusa(double cateto1, double cateto2) {
double risultato = sqrt(cateto1 * cateto1 + cateto2 * cateto2);
return risultato;
}
Ora, possiamo invocare questa funzione in questo modo:
double lunghezza = ipotenusa(3.0, 4.0);
Il valore di lunghezza sarà 5.0
. Ma questo risultato sarà sempre lo stesso a parità di argomenti passati alla funzione. In altre parole, se invochiamo la funzione ipotenusa(3.0, 4.0)
il risultato sarà sempre 5.0
indipendentemente dal contesto e da quante volte invochiamo la funzione.
La variabile risultato
è una variabile locale invisibile al di fuori della funzione ipotenusa
. Questo significa che la funzione ipotenusa
non modifica lo stato globale del programma.
Ma cosa si intende per stato globale del programma? Si intende lo stato delle variabili globali e delle variabili statiche. Se una funzione modifica una variabile globale o una variabile statica, allora la funzione non è pura.
Stato Globale di un Programma
Lo stato globale di un programma è composto da:
- Variabili globali e loro contenuto attuale;
- Variabili statiche e loro contenuto attuale.
Prendiamo un esempio di funzione impura:
int contatore = 0;
int funzione(int x) {
contatore += x;
return x + 1;
}
In questo caso, è vero che la funzione funzione
restituisce sempre x + 1
, ma modifica lo stato globale del programma incrementando il valore della variabile globale contatore
. Quindi la seconda condizione è violata.
Analogamente, se una funzione legge lo stato globale del programma, allora non è pura. Ad esempio:
int variabile_globale = 10;
int funzione(int x) {
return x + variabile_globale;
}
In questo caso, la funzione funzione
legge il valore della variabile globale variabile_globale
. Anche se non modifica lo stato globale, leggere lo stato globale è sufficiente per rendere la funzione impura in quanto basta che il valore della variabile globale cambi per ottenere un risultato diverso. Si viola quindi la prima condizione.
Funzione Pura
Una funzione si dice pura se:
- A parità di argomenti passati come parametri restituisce sempre lo stesso risultato;
- Accede solo alle variabili locali.
Il nome puro deriva dal fatto che una funzione di questo tipo si avvicina molto al concetto di funzione matematica.
Nella pratica una funzione in linguaggio C e una funzione matematica sono entità diverse. Una funzione matematica è una relazione tra due insiemi di valori, e non può restituire valori diversi a parità di argomenti.
Prendiamo, ad esempio, le funzioni matematiche seno e coseno. Queste funzioni restituiscono sempre lo stesso valore a parità di argomenti:
Le funzioni seno e coseno restituiranno sempre questi valori a parità di argomenti. Questo è il concetto di funzione pura.
Analogamente, le funzioni C che calcolano il seno ed il coseno, rispettivamente sin
e cos
, restituiscono sempre lo stesso valore a parità di argomenti. Queste funzioni sono quindi funzioni pure.
Ma in linguaggio C possiamo anche realizzare, come visto sopra, funzioni impure. Queste funzioni, pur non essendo matematiche, sono comunque utili per realizzare programmi complessi.
Funzione Impura
Una funzione si dice impura se viola una delle due condizioni delle funzioni pure.
Le funzioni di I/O sono impure per definizione
Vi è un altro caso in cui le funzioni possano essere impure: il caso in cui facciano uso di funzionalità di I/O, o input/output.
Esempi di I/O sono la scrittura/lettura da console, la scrittura/lettura su file, la connessione ad un database, ecc.
Un esempio classico è la scanf
che legge da console. Questa funzione legge da console e restituisce il valore letto. Questo valore dipende dallo stato della console, quindi la funzione scanf
è impura.
int x;
scanf("%d", &x);
Anche la funzione printf
è impura in quanto scrive su console.
printf("Hello, World!\n");
In generale, tutte le funzioni che leggono o scrivono da/verso l'esterno sono impure.
Funzioni che usano I/O sono impure
Le funzione che usano funzionalità di I/O sono impure:
- Lettura/Scrittura da/verso console;
- Lettura/Scrittura su file;
- Connessione ad un database;
- Connessione via rete.
e così via.
Effetti Collaterali
Adesso che abbiamo chiaro il concetto di Funzione Pura e Funzione Impura possiamo riprendere ed approfondire un concetto che abbiamo già incontrato in precedenza: gli effetti collaterali.
Abbiamo studiato già il fatto che gli operatori possano avere effetti collaterali. In altre parole, ci sono operatori che non solo leggono il valore dei propri operandi ma li modificano.
Ad esempio:
int x = 10;
int y = 20;
int c = (++x) - (y--);
In questo esempio, l'operatore ++
incrementa il valore di x
e l'operatore --
decrementa il valore di y
. Questi operatori hanno effetti collaterali sui propri operandi.
Il concetto di Effetto Collaterale può essere esteso al caso della modifica dello stato globale del programma.
In particolare una funzione impura ha effetti collaterali in quanto modifica lo stato globale del programma. Cosa che non avviene per le funzioni pure.
Effetto Collaterale per le Funzioni
Si dice che una funzione ha un Effetto Collaterale quando ha un qualunque effetto osservabile oltre alla lettura dei valori dei parametri e alla restituzione di un valore.
Quindi la seconda condizione delle funzioni pure può essere riformulata come:
Seconda Condizione delle Funzioni Pure
Condizione necessaria affinché una funzione sia pura è che non abbia effetti collaterali.
Ordine di Valutazione delle Espressioni ed Effetti Collaterali
A prima vista, i concetti di funzione pura, funzione impura ed Effetto Collaterale sembrano solo concetti teorici. Definizioni fini a se stesse.
Esiste, però, in linguaggio C così come in altri linguaggi di programmazione un'importante implicazione che riguarda l'ordine di valutazione delle espressioni.
Abbiamo già affrontato, in una lezione precedente, che tra gli operatori di un'espressione il linguaggio C impone un'ordine di valutazione.
Ad esempio, prendiamo l'espressione che segue:
int x = 10;
int y = 20;
int z = 30;
int risultato = x * y + z;
In questo caso, l'operatore *
ha la precedenza sull'operatore +
. Quindi l'espressione x * y
viene valutata prima dell'espressione x * y + z
.
Lo standard del C impone delle regole ben precise sulla precedenza degli operatori e tutti i compilatori devono rispettarle.
Tuttavia, lo standard del C non impone alcuna regola sull'ordine di valutazione degli operandi. Finora, non ci siamo ancora scontrati con questo problema, perché le nostre espressioni contenevano solo variabili. Valutare un operando che sia una variabile significa, semplicemente, leggerne il contenuto. Ma cosa accade se mescoliamo espressioni e invocazioni a funzione?
Prendiamo un esempio di espressione che contiene più chiamate a funzione:
int risultato = funzione1(x) * funzione2(y) + funzione3(z);
In questo caso, l'ordine di valutazione dell'espressione, in base alle regole del C, sarà il seguente:
- Prima la moltiplicazione:
funzione1(x) * funzione2(y)
; - Poi la somma:
(funzione1(x) * funzione2(y)) + funzione3(z)
; - Infine l'assegnamento:
risultato = ((funzione1(x) * funzione2(y)) + funzione3(z))
.
Ma lo standard del C si ferma qui. Se prendiamo la moltiplicazione:
funzione1(x) * funzione2(y)
prima di poterla effettuare, dobbiamo valutare gli operandi, cioè dobbiamo invocare le due funzioni funzione1
e funzione2
e prenderne i risultati. Ma in che ordine verranno eseguite le due funzioni?
Purtroppo lo standard del C non impone nessuna regola in tal senso. Si potrebbe fare l'assunzione che gli operandi vengano valutati da sinistra a destra, cioè venga prima valutata funzione1(x)
e poi funzione2(y)
. Ma non è detto che ciò accada.
Infatti, un compilatore potrebbe applicare delle ottimizzazioni ed eseguire le due funzioni in un ordine differente.
Quali sono le conseguenze?
Ecco che entrano in gioco i concetti di funzione pura e funzione impura. Se le funzioni funzione1
, funzione2
e funzione3
sono funzioni pure, allora l'ordine di valutazione delle espressioni non avrà alcuna importanza. Infatti, a parità di argomenti passati, il risultato sarà sempre lo stesso.
Ciò non accade se le funzioni sono impure perché potrebbero avere effetti collaterali.
Prendiamo un esempio:
int contatore = 0;
int funzione1(int x) {
contatore += x;
return contatore + 1;
}
int funzione2(int x) {
contatore -= x;
return contatore + 2;
}
/* ... */
int risultato = funzione1(10) * funzione2(20);
In tal caso, il valore di risultato
dipende dall'ordine di valutazione delle due funzioni:
-
Se viene valutata prima la funzione 1:
contatore
diventa10
;funzione1(10)
restituisce11
;contatore
diventa-10
;funzione2(20)
restituisce-8
;risultato
diventa11 * -8 = -88
.
-
Se viene valutata prima la funzione 2:
contatore
diventa-20
;funzione2(20)
restituisce-18
;contatore
diventa-10
;funzione1(10)
restituisce-11
;risultato
diventa-11 * -18 = 198
.
L'ordine di valutazione ha avuto impatti sul risultato finale.
Il problema è proprio che le due funzioni hanno effetti collaterali sul programma: entrambe modificano la variabile globale contatore
.
Questo esempio ci mostra come il concetto di funzione pura e funzione impura non sia solo teorico ma abbia delle ripercussioni importanti sullo sviluppo di codice C.
In generale vale che:
Valutazione di un'espressione ed Effetti collaterali
L'ordine di valutazione degli operandi di un'espressione in linguaggio C non è definito.
Tuttavia, esso è ininfluente ai fini del risultato se vengono rispettate le seguenti condizioni:
- Si usano solo funzioni pure;
- Non si utilizza il risultato di operatori con effetti collaterali, come
++
e--
.
Se le due condizioni di sopra non vengono rispettate potremmo ottenere dei risultati diversi da quelli attesi.
Pertanto, ci sentiamo di dare due consigli:
Evitare di combinare funzioni impure in espressioni
Il primo consiglio è quello di evitare di usare funzioni impure in espressioni, oppure, se proprio necessario non usare più di una funzione impura in un'espressione.
Imporre un ordine di valutazione degli operandi
Se proprio siamo costretti ad usare più di una funzione impura in un'espressione, possiamo imporre un ordine di valutazione degli operandi.
Per far questo possiamo usare delle variabili temporanee.
Tornando all'esempio di sopra, per essere sicuri che funzione1
venga valutata prima di funzione2
possiamo scrivere:
int risultato1 = funzione1(10);
int risultato2 = funzione2(20);
int risultato = risultato1 * risultato2;
In questo modo, siamo sicuri che funzione1
venga valutata prima di funzione2
.
Conclusioni
In questa lezione abbiamo approfondito il concetto di funzione pura e funzione impura. Abbiamo visto che una funzione pura è una funzione che restituisce sempre lo stesso risultato a parità di argomenti e non modifica lo stato globale del programma.
Abbiamo visto che le funzioni impure, invece, violano una delle due condizioni delle funzioni pure.
Abbiamo visto che le funzioni che fanno uso di funzionalità di I/O sono impure per definizione.
Abbiamo introdotto il concetto di effetto collaterale per le funzioni e abbiamo visto che una funzione ha un effetto collaterale quando ha un qualunque effetto osservabile oltre alla lettura dei valori dei parametri e alla restituzione di un valore.
Infine, abbiamo visto che l'ordine di valutazione delle espressioni in linguaggio C non è definito e che, se si usano funzioni impure, potrebbero esserci delle conseguenze importanti sul risultato finale.
Nella prossima lezione studieremo un concetto fondamentale riguardante le funzioni in linguaggio C: lo Stack di Chiamata.