Go - from Google



Funzioni   (2)       

Considerazioni di carattere generale intorno alle funzioni riguardano anche lo "scope" ovvero le regole di visibilità delle variabili. Cominciamo con il prendere in considerazione il caso più semplice:

  esempio 5.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"

var x = 10

func main(){
  fmt.Println(x)
  f1()
  f2()
}

func f1() {
  fmt.Println(x)
}

func f2() {
  fmt.Println(x)
}

La variabile x è esterna a tutte le funzioni, compreso il main. Essa è una variabile globale risultando pertanto visibile ad ogni livello gerarchicamente sottostante come dimostra l'output che è il seguente:

10
10
10


Modifichiamo la funzione f1 come segue:

func f1() {
  fmt.Println(x)
  x = 8
  fmt.Println(x)
}

L'output diventa:

10
10
8
8


e questo perchè è stato fatto un assegnamento sulla variabile globale. Se invece effettuiamo la modifica come segue:

func f1() {
  fmt.Println(x)
  x := 8
  fmt.Println(x)
}

avremo come output:

10
10
8
10


in quanto stavolta abbiamo effettuato una definizione in locale che copre quella globale. E' banale ma c'è, ovviamente, da stare attenti a non effettuare modifiche accidentali su variabili globali. Va da sè che variabili definite internamente ad una singola funzione sono invece visibili solo all'interno della funzione stessa.

Detto questo torniamo a parlare dei nostri parametri, in particolare parleremo della possibilità offerta da Go di proporre funzioni in grado di accettare un numero variabile di parametri, quindi anche un diverso numero di questi a seconda della chiamata delle funzione effettuata. L'unica limitazione è che i parametri il cui numero è variabile possono essere di un solo tipo (esiste come vedremo il sistema per superare questa limitazione). Il modo per dire al compilatore che avremo un numero variabile di parametri di un certo tipo è indicare, dopo l'ultimo parametro:

id ...tipo

a quel punto potremo offrire da 0 ad un numero teoricamente infinito di parametri di quel tipo. Bisogna precisare che i parametri variabili vengono gestiti attraverso slices: per cui per una piena comprensione, rimando al capitolo che tratta appunto di array e slice. Vediamo l'esempio:

  esempio 5.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"

func main(){
f1("somma = ", 0,2,4,6,8)
f1("somma = ", 10, 20, 30)
}

func f1 (tipo string, num ...int){
  fmt.Print(tipo)
  var x int = 0
  for n1 := 0; n1 < len(num); n1++ {
    x = x + num[n1]
  }
fmt.Println(x)
}

La riga 9 ci presenta la possibilità di inserire un numero variabile di parametri e questo viene fatto alla 5 ed alla 6. Parlando di array e slice questo esempio sarà più chiaro.
Nel caso in cui invece si voglia avere la possibilità di avere varie tipologie di parametri di dovrà usare una struct.
Un'altra interessante possibilità ci viene offerta dalla keyword defer. Questa istruzione permette di rimandare l'esecuzione di una funzione fino al termine di quella all'interno della quale è stata chiamata, ivi compresi eventuali return; quindi appena prima della parentesi graffa di chiusura.

  esempio 5.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"

func main(){
  f1()
  defer f2()
  f1()
  fmt.Println("Finito!")
}

func f1 (){
  fmt.Println("Ciao")
}

func f2() {
  fmt.Println("amico")
}

Per rendersi conto del dell'effetto di defer basta eseguire il programma con e senza di esso. L'output cambia:

output con defer output senza defer
Ciao
Ciao
Finito!
amico
Ciao
amico
Ciao
Finito!

questo appunto perchè defer permette l'esecuzione di f2 solo quanto tutto il resto dell'atttività della funzione chiamante è terminata; nel primo caso quindi l'esecuzione di f2, ancorchè chiamato dopo subito dopo f1, arriva giusto alla fine, dopo che è stata eseguita anche l'istruzione alla riga 8. Defer verrà molto utile in tante situazioni pratiche ben più significative di questo esempio didattico. Una domanda interessante potrebbe riguardare la valutazione dei parametri, quando ci sono,  trattati da una funzione differita, soprattutto quando questi sono oggetto di elaborazioni successive alla chiamata: bene il valore di questi parametri è quello calcolato al momento della chiamata non a quello della esecuzione. Ecco l'esempio:

  esempio 5.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"

func main(){
  var x rune = 0
  f1(x)
  defer f2(x)
  x = x + 1
  f1(x)
  fmt.Println("Finito!")
}

func f1 (x rune){
  fmt.Println(x)
}

func f2(x rune) {
  fmt.Println(x)
}

e il relativo ouput:

0
1
Finito!
0

che evidenzia come la funzione f2 riceva il parametro al momento effettivo della chiamata e non quando viene eseguito. Il primo 0 è legato all'output richiesto alla riga 6, il successivo 1 è chiamato alla 9, poi arriva la stringa "finito!" (riga 10) e infine l'ultimo 0 che dipende dal differimento richiesto alla riga 7.
Un'ultima questione legata al differimento è la seguente: cosa succede se ho più funzioni differite? La cosa è forse meno intuitiva di quanto si possa pensare pensare perchè, in realtaà, non si segue l'ordine "dall'alto in basso" come si potrebbe erroneamente pensare, bensì il classico LIFO (Last In First Out, per chi non se lo ricorda), quindi l'ultima funzione che accede alla ipotetica coda di attesa è la prima ad essere eseguita. L'esempio che segue chiarisce la cosa:

  esempio 5.5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import "fmt"

func main(){
  defer f1()
  defer f2()
  defer f3()
  fmt.Println("Ecco le 3 funzioni")
}

func f1 (){
  fmt.Println("Sono f1")
}

func f2() {
  fmt.Println("Sono f2")
}

func f3() {
  fmt.Println("Sono f3")
}

Che espone il seguente output:

Ecco le 3 funzioni
Sono f3
Sono f2
Sono f1


quindi f3 va in esecuzione per prima.

Dal momento che il concetto di funzione è spesso legato a quello di ricorsione, presento l'esempio più classico in questo senso, quello del calcolo della sequenza di Fibonacci. Come nota relativa al concetto di ricorsione è noto che questo procedimento si scontra a volta con il problema dello stack overflow. Come vedremo di sono dei meccanismi atti a combattere questa criticità, meccanismi che si basano su valutazioni lazy (pigre, in pratica le funzioni vengono valutato solo quando effettivamente necessario) e sulle goroutines, argomento di cui dovremo parlare molto approfonditamente.

  esempio 5.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"

func main() {
  result := 0
  for i:=0; i <= 10; i++ {
  result = fibonacci(i)
  fmt.Println(result)
  }
}

func fibonacci(n int) (res int){
  if n <= 1 {
  res = 1
  } else {
res = fibonacci(n -1) + fibonacci (n -2)
}
  return
}

Tutto sommato una implementazione molto simile a quella che si trova in altri linguaggi.

Infine vediamo un altro procedimento molto interessante ovvero la possibilità di passare funzioni come parametri altre funzioni: L'esempio che segue è il più semplice che ho potuto trovare, francamente non ne ho in mente uno più semplice ed illustra bene il concetto:

  esempio 5.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"

func main() {
  f2(3, f1)
}

func f1(a, b int){
  fmt.Println(a + b)
}

func f2(x int, f func(int, int)){
  f(x, 5)
}

come si vede alla riga 5 la funzione f2 viene richiamata con due parametri, un intero ed una funzione, corrispondentemente a quanto richiesto dalla firma alla riga 12. Sempre alla riga 5 la funzione che viene passata come parametro è f1, definita alla 8 e, ovviamente, opportunamente conformata dal punto di vista dei parametri. Alla riga 12 infatti viene richiesto che la funzione che sarà passata come parametro abbia a sua volta due interi come parametri.
L'esempio non è molto diverso se presumiamo anche dei valori di ritorno (nel caso presente costituiscono solo una inutile complicazione).

  esempio 5.8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"

func main() {
  var x = f2(3,f1)
  fmt.Println(x)
}

func f1(a, b int)(res int){
  return(a + b)
}

func f2(x int, f func(int, int)(int))(int){
  return f(x, 5)
}