Affinchè si possa realmente costruite le dinamiche di cui si ha bisogno nel programma, è necessario che l'uso
delle funzioni sia sufficientemente flessibile. Per tale motivo, in questa lezione incontriamo una serie di tecniche che permettano
non solo di scrivere funzioni ma anche di sfruttarle a proprio piacimento.
Riferimenti a funzione
Per prima cosa, notiamo che il nome di una funzione può essere trattato proprio come un normale valore da assegnare ad una variabile
ed invocato. Supponiamo di avere a disposizione le seguenti funzioni:
func somma(a int,b int) int{
return a+b
}
func moltiplica(a int,b int) int{
return a*b
}
La prima esegue la somma tra due numeri interi restituendone il risultato mentre la seconda si occupa di moltiplicare i due
argomenti e rispondere con il loro prodotto. Possiamo immaginare i loro nomi come una sorta di riferimento al blocco di codice collegato.
Se ad esempio scrivessimo:
x:=somma
Non avremmo fatto altro che creare una variabile x
che punta allo stesso blocco di codice di
somma
. In questo modo, per invocare la funzionalità collegata potremmo utilizzare ugualmente sia
x
sia somma
:
fmt.Println(somma(10,8))
fmt.Println(x(10,8))
ottenendo in entrambi i casi il valore 18.
Tale tecnica è utile anche per rendere più dinamico il codice ad esempio cambiando il valore assegnato allo stesso
alias sostituendo così "al volo" la funzione da eseguire:
x:=somma
// ora x è somma
fmt.Println(x(5,6))
x=moltiplica
// ora x è moltiplica
fmt.Println(x(5,6))
L'output che si ottiene è:
11
30
quindi il riferimento x
con il cambio di assegnazione ha attivato una volta la funzione
somma
, una volta moltiplica
pur mantenendo identica la forma "esteriore" x(5,6)
.
Si noti inoltre che alla prima assegnazione abbiamo utilizzato :=
mentre alla seconda
il semplice simbolo di uguale e ciò perchè prima la variabile x
non esisteva pertanto
trattavasi di dichiarazione con inizializzazione mentre alla seconda assegnazione era
già esistente e ci accingevamo solo a reimpostarne il valore.
Funzioni anonime
Con il linguaggio Go si può anche creare le funzioni anonime ovvero blocchi di codice che non dispongono di un proprio
nome iniziale e che possono essere usate mediante riferimento. Ad esempio, usiamo qui una funzione
anonima per inizializzare direttamente x
:
x:=func(a int,b int) int{
return a+b
}
fmt.Println(x(9,5))
Come si vede, il blocco di codice che assegniamo alla variabile non ha alcun nome ma definisce solo gli aspetti salienti della
funzione quali argomenti, tipo di ritorno e operazioni da eseguire. In fin dei conti finora non abbiamo fatto niente di eccezionale se non
cambiare il modo in cui abbiamo assegnato un nome al blocco di codice. Osserviamo però cosa possiamo
fare nel seguente caso. Creiamo una funzione che tra i suoi argomenti ne contempla uno il cui tipo di dato è
func(int,int) int
ovvero una generica funzione che accetta due interi come argomenti e ne restituisce uno come
risultato:
func operazione_generica(f func(int,int) int, a int,b int) int{
return f(a,b)
}
Cosa fa la funzione operazione_generica
esattamente? Non possiamo saperlo solo osservando i suoi argomenti,
sappiamo però che userà il secondo ed il terzo (i due interi) per eseguire la funzione passata come primo, il parametro
f
. Eseguendo infatti:
operazione_generica(func(a int, b int) int{return a*b}, 9, 5)
otteniamo 45 perchè la funzione anonima inoltrata come primo argomento restituisce il prodotto degli argomenti mentre
eseguendo:
operazione_generica(func(a int, b int) int{return a-b}, 9, 5)
otteniamo 4 perchè questa volta la funzione che all'interno di operazione_generica
è rappresentata
da f
porta con sè l'esecuzione di una sottrazione.
Closure
Il percorso seguito sinora in questa lezione ci porta dritti dritti alle closure. Si tratta di un meccanismo molto potente, disponibile
in vari linguaggi di programmazione che permette di creare un contesto, all'interno di una funzione, in cui rimangano persistenti in memoria
i valori delle variabili dichiarate anche dopo la fine dell'invocazione alla funzione. Come schema generale,
possiamo dire che una closure inizia con una dichiarazione particolare in cui il nome della funzione è
seguito da una sorta di dichiarazione di funzione anonima:
func esecuzione() func() string {
lo string
che appare in fondo è il tipo di dato finale che verrà restituito (come si immagina,
potrebbe essere un qualsiasi altro tipo) e all'interno della closure che nel nostro caso prende il nome di
esecuzione avremo: la dichiarazione delle variabili che rimarranno persistenti tra una chiamata e l'altra a
esecuzione; un return
che restituirà la funzione che costituirà il vero e proprio blocco operativo
della closure. Vediamo un esempio.
Supponiamo che lo scopo della funzione anonima restituita da esecuzione sia quello di
stampare l'ora attuale. La closure avrà al suo interno una variabile, quante
, che ad ogni
invocazione viene incrementata di un'unità. La nostra prova consisterà nell'invocare più volte, ogni due
secondi, la closure dimostrando che nonostante l'ora stampata cambi sempre la variabile quante
continui a mantenere il suo valore
trasversalmente alle chiamate:
package main
import ("fmt"
"time")
func esecuzione() func() string {
quante := 0
return func() string{
quante += 1
fmt.Printf("Esecuzione n. %d\n", quante)
return time.Now().Format("03:04:05")
}
}
func main() {
da_chiamare := esecuzione()
fmt.Println(da_chiamare())
time.Sleep(2 *time.Second)
fmt.Println(da_chiamare())
time.Sleep(2 *time.Second)
fmt.Println(da_chiamare())
}
L'output prodotto è il seguente:
Esecuzione n. 1
11:00:00
Esecuzione n. 2
11:00:02
Esecuzione n. 3
11:00:04
L'esperimento è pertanto riuscito ma si noti che all'inizio del main
abbiamo prima invocato
la closure con da_chiamare := esecuzione()
per crearne un'istanza e successivamente, per far
funzionare il meccanismo, abbiamo dovuto invocare sempre il riferimento da_chiamare
per
mantenere la continuità tra le invocazioni.
Infine, si noti che per la temporizzazione e la stampa dell'ora attuale abbiamo chiamato in causa il
package time
che verrà approfondito nel seguito. Per il momento, ci si accontenti di tenere
presente che con time.Now().Format("03:04:05")
si stampa l'ora attuale mentre
con time.Sleep(2 *time.Second)
chiediamo l'interruzione dell'esecuzione per due secondi.