6 La funzione
La funzione è un blocco di codice riutilizzabile che contiene una sequenza di istruzioni. Questi costrutti sono fondamentali per la strutturazione e la modularizzazione del codice, consentendo di definire operazioni che possono essere invocate più volte durante l’esecuzione di un programma. La distinzione tra funzioni e metodi è che le funzioni sono indipendenti, mentre i metodi sono associati a oggetti o classi.
6.1 Dichiarazione
La dichiarazione di una funzione è il processo mediante il quale si definisce una nuova funzione nel programma, specificandone il nome, i parametri (se presenti) e il blocco di codice che essa eseguirà. Questo processo informa il compilatore o l’interprete che una certa funzione esiste e può essere utilizzata nel codice. Durante la dichiarazione, non viene eseguito alcun codice; viene semplicemente definita la funzione in modo che possa essere invocata successivamente nel programma.
Esempio in Python:
- 1
-
Definizione della funzione
somma
con due parametria
eb
. - 2
-
La funzione
somma
ritorna la somma dei parametria
eb
.
6.2 Il parametro e l’argomento
Il parametro e l’argomento sono strumenti fondamentali per passare dati alle funzioni e influenzarne il comportamento. In particolare:
Parametri o parametri formali: I parametri sono definiti nella dichiarazione della funzione e rappresentano i nomi delle variabili che la funzione utilizzerà per accedere ai dati passati.
Argomenti o parametri attuali: Gli argomenti sono i valori effettivi passati alla funzione quando viene chiamata.
Esempio in Python:
- 1
-
a
eb
sono parametri della funzionesomma
. - 2
-
3
e4
sono argomenti passati alla funzionesomma
. - 3
-
Il risultato della funzione
somma
viene stampato.
6.3 Il valore di ritorno
Il valore di ritorno è il risultato prodotto da una funzione, che può essere utilizzato nell’istruzione chiamante. Una funzione può restituire un valore utilizzando una sintassi particolare come la parola chiave return
.
Esempio in Java:
public class Main {
1 public static int somma(int a, int b) {
2 return a + b;
}
public static void main(String[] args) {
3 int result = somma(3, 4);
4 System.out.println(result);
}
}
- 1
-
Dichiarazione della funzione
somma
che accetta due parametri interi. - 2
-
La funzione
somma
ritorna la somma dia
eb
. - 3
-
Chiamata della funzione
somma
con argomenti3
e4
. - 4
-
Il risultato della funzione
somma
viene stampato.
6.4 Ambito e visibilità
L’ambito e la visibilità degli identificatori delle funzioni sono concetti sono simili a quelli delle variabili, ma presentano alcune differenze chiave che è importante comprendere.
6.4.1 Ambito
Per le funzioni distinguiamo sempre i seguenti:
Ambito globale: Una funzione dichiarata a livello globale, cioè al di fuori di qualsiasi altra funzione o blocco di codice, ha un ambito globale. Questo significa che la funzione è visibile e può essere chiamata da qualsiasi punto del programma dopo la sua dichiarazione.
Esempio in C++:
#include <iostream> 1void funzioneGlobale() { std::cout << "Funzione globale" << std::endl; } int main() { 2 funzioneGlobale(); return 0;
- 1
-
Dichiarazione della funzione
funzioneGlobale
a livello globale. - 2
-
Chiamata della funzione
funzioneGlobale
all’interno dimain
.
Il comportamento è identico in Java e C. In Python, le funzioni definite a livello globale hanno ambito globale.
Ambito locale: Una funzione dichiarata all’interno di un blocco di codice (come all’interno di una funzione o di una classe) ha un ambito locale. La funzione è visibile e può essere chiamata solo all’interno di quel blocco.
Esempio in Python:
def funzione_esterna(): 1 def funzione_locale(): print("Funzione locale") 2 funzione_locale() funzione_esterna() 3funzione_locale()
- 1
-
Dichiarazione della funzione
funzione_locale
all’interno difunzione_esterna
. - 2
-
Chiamata della funzione
funzione_locale
all’interno difunzione_esterna
. - 3
-
Chiamata a
funzione_locale
al di fuori difunzione_esterna
, che genera un errore poichéfunzione_locale
non è visibile a questo livello.
In Java e C++, le funzioni dichiarate all’interno di un blocco (come metodi all’interno di una classe) sono accessibili solo all’interno di quel blocco, simile a Python.
In C, le funzioni locali non sono standard, ma è possibile ottenere un comportamento simile usando funzioni statiche o funzioni inline definite all’interno di un file sorgente specifico.
6.4.2 Visibilità
La visibilità si riferisce a dove nel codice l’identificatore di una funzione può essere utilizzato. La visibilità è strettamente legata all’ambito, ma può essere influenzata anche da altre considerazioni come la modularità e le regole di accesso.
Visibilità Globale: Le funzioni con ambito globale sono visibili ovunque nel programma come per le variabili.
Visibilità Locale: Le funzioni con ambito locale sono visibili solo all’interno del blocco in cui sono dichiarate. Questo è utile per creare funzioni di supporto (inglese: helper) o interne che non devono essere accessibili dall’esterno.
Esempio in Python:
def funzione_esterna(): 1 def funzione_supporto(): print("Funzione di supporto") 2 funzione_supporto() print("Funzione esterna") funzione_esterna()
- 1
-
Dichiarazione della funzione
funzione_supporto
all’interno difunzione_esterna
. - 2
-
Chiamata della funzione
funzione_supporto
all’interno difunzione_esterna
.
6.4.3 Differenze tra funzioni con variabili e oggetti
Sebbene l’ambito e la visibilità delle funzioni condividano concetti simili con le variabili e gli oggetti, ci sono alcune differenze chiave:
Durata di vita: Le variabili locali (automatiche) hanno una durata di vita limitata al blocco di codice in cui sono dichiarate. Quando il controllo esce dal blocco, la memoria allocata per la variabile viene liberata. Le funzioni, tuttavia, non vengono “distrutte” quando il controllo esce dal loro ambito; semplicemente non sono più visibili e chiamabili. In Python, le variabili definite all’interno di un blocco di un’istruzione composta rimangono accessibili finché sono nello stesso ambito di funzione o modulo, mentre le funzioni definite all’interno di un’altra funzione (nested functions) sono visibili solo all’interno di quella funzione.
Allocazione dinamica: In C++, le variabili e gli oggetti possono essere allocati dinamicamente usando
new
e deallocati usandodelete
. Le funzioni non richiedono un’allocazione esplicita di memoria; la loro dichiarazione è sufficiente per renderle utilizzabili nell’ambito definito.
6.5 La ricorsione
La ricorsione è la capacità di una funzione di chiamare se stessa, utile per risolvere problemi che possono essere suddivisi in sottoproblemi simili. Ogni chiamata ricorsiva deve avvicinarsi a una condizione di terminazione per evitare loop infiniti.
Esempio in C++ (calcolo del fattoriale):
#include <iostream>
1int fattoriale(int n) {
2 if (n <= 1) return 1;
3 return n * fattoriale(n - 1);
}
int main() {
4 int result = fattoriale(5);
5 std::cout << result << std::endl;
return 0;
}
- 1
-
Dichiarazione della funzione
fattoriale
. - 2
-
Condizione di terminazione: se
n
è minore o uguale a 1, ritorna 1. - 3
-
Chiamata ricorsiva:
fattoriale
chiama se stessa conn - 1
. - 4
-
Chiamata della funzione
fattoriale
con argomento5
. - 5
-
Il risultato della funzione
fattoriale
viene stampato.
6.6 La funzione in prima classe
Il concetto di funzione in prima classe (inglese: first-class function) è un principio fondamentale in molti linguaggi di programmazione, particolarmente rilevante nel paradigma di programmazione funzionale. In breve, un linguaggio di programmazione che supporta le funzioni come cittadini di prima classe. Ciò significa che le funzioni possono essere manipolate e utilizzate come qualsiasi altro tipo di dato. Le operazioni che definiscono questa caratteristica includono:
Assegnazione a variabili: Le funzioni possono essere assegnate a variabili.
Passaggio come argomenti: Le funzioni possono essere passate come argomenti ad altre funzioni.
Restituzione come risultati: Le funzioni possono essere restituite da altre funzioni.
Memorizzazione in strutture dati: Le funzioni possono essere memorizzate in strutture dati come liste, dizionari, ecc.
Nel paradigma di programmazione funzionale, le funzioni in prima classe sono essenziali perché permettono di trattare le funzioni pure come valori di prima classe. Le funzioni pure sono funzioni il cui output è determinato solo dai loro input e non hanno effetti collaterali. L’abilità di passare, restituire e comporre funzioni in prima classe è fondamentale per il paradigma funzionale, poiché consente di creare funzioni di ordine superiore e di mantenere l’immutabilità. Le funzioni in prima classe permettono di:
Creare funzioni di ordine superiore: Funzioni che accettano altre funzioni come argomenti o che restituiscono funzioni, promuovendo un’astrazione più elevata e la riutilizzabilità del codice.
Comporre funzioni: Combinare semplici funzioni pure per costruire funzioni più complesse, facilitando la costruzione di software modulare e mantenibile.
Favorire l’immutabilità: Favorire la scrittura di codice che non modifica lo stato, riducendo i bug e rendendo il codice più prevedibile.
6.6.1 Implementazione in linguaggi di programmazione
Python tratta le funzioni come oggetti in prima classe. Ecco come:
# Assegnazione a variabili
1def saluto(nome):
return f"Ciao, {nome}!"
2messaggio = saluto
3print(messaggio("Mondo"))
# Passaggio come argomenti
4def chiamata_di_ritorno(f):
return f("Mondo")
5print(chiamata_di_ritorno(saluto))
# Restituzione come risultati
6def crea_saluto():
7 def saluto(nome):
return f"Ciao, {nome}!"
8 return saluto
9saluta = crea_saluto()
10print(saluta("Mondo"))
- 1
-
Definizione della funzione
saluto
che accetta un parametronome
. - 2
-
Assegnazione della funzione
saluto
alla variabilemessaggio
. - 3
-
Chiamata della funzione
messaggio
con l’argomento"Mondo"
, che stampa “Ciao, Mondo!”. - 4
-
Definizione della funzione
chiamata_di_ritorno
che accetta una funzione come parametrof
. - 5
-
Chiamata della funzione
chiamata_di_ritorno
con la funzionesaluto
come argomento, che stampa “Ciao, Mondo!”. - 6
-
Definizione della funzione
crea_saluto
che restituisce una funzione. - 7
-
Definizione della funzione
saluto
interna acrea_saluto
. - 8
-
Restituzione della funzione
saluto
dacrea_saluto
. - 9
-
Assegnazione della funzione restituita da
crea_saluto
alla variabilesaluta
. - 10
-
Chiamata della funzione
saluta
con l’argomento"Mondo"
, che stampa “Ciao, Mondo!”.
Anche JavaScript supporta le funzioni in prima classe:
// Assegnazione a variabili
1function saluto(nome) {
return `Ciao, ${nome}!`;
}
2let messaggio = saluto;
3console.log(messaggio("Mondo"));
// Passaggio come argomenti
4function chiamataDiRitorno(f) {
return f("Mondo");
}
5console.log(chiamataDiRitorno(saluto));
// Restituzione come risultati
6function creaSaluto() {
7 function saluto(nome) {
return `Ciao, ${nome}!`;
}
8 return saluto;
}
9let saluta = creaSaluto();
10console.log(saluta("Mondo"));
- 1
-
Definizione della funzione
saluto
che accetta un parametronome
. - 2
-
Assegnazione della funzione
saluto
alla variabilemessaggio
. - 3
-
Chiamata della funzione
messaggio
con l’argomento"Mondo"
, che stampa “Ciao, Mondo!”. - 4
-
Definizione della funzione
chiamataDiRitorno
che accetta una funzione come parametrof
. - 5
-
Chiamata della funzione
chiamataDiRitorno
con la funzionesaluto
come argomento, che stampa “Ciao, Mondo!”. - 6
-
Definizione della funzione
creaSaluto
che restituisce una funzione. - 7
-
Definizione della funzione
saluto
interna acreaSaluto
. - 8
-
Restituzione della funzione
saluto
dacreaSaluto
. - 9
-
Assegnazione della funzione restituita da
creaSaluto
alla variabilesaluta
. - 10
-
Chiamata della funzione
saluta
con l’argomento"Mondo"
, che stampa “Ciao, Mondo!”.
Haskell è un linguaggio puramente funzionale che supporta naturalmente le funzioni in prima classe:
-- Assegnazione a variabili
1saluto :: String -> String
saluto nome = "Ciao, " ++ nome ++ "!"
2messaggio = saluto
3main = putStrLn (messaggio "Mondo")
-- Passaggio come argomenti
4chiamataDiRitorno :: (String -> String) -> String
chiamataDiRitorno f = f "Mondo"
5main = putStrLn (chiamataDiRitorno saluto)
-- Restituzione come risultati
6creaSaluto :: String -> String
creaSaluto = saluto
7main = putStrLn (creaSaluto "Mondo")
- 1
-
Definizione della funzione
saluto
che accetta una stringanome
e restituisce una stringa. - 2
-
Assegnazione della funzione
saluto
alla variabilemessaggio
. - 3
-
Chiamata della funzione
messaggio
con l’argomento"Mondo"
, che stampa “Ciao, Mondo!”. - 4
-
Definizione della funzione
chiamataDiRitorno
che accetta una funzione come parametrof
. - 5
-
Chiamata della funzione
chiamataDiRitorno
con la funzionesaluto
come argomento, che stampa “Ciao, Mondo!”. - 6
-
Definizione della funzione
creaSaluto
che restituisce la funzionesaluto
. - 7
-
Chiamata della funzione
creaSaluto
con l’argomento"Mondo"
, che stampa “Ciao, Mondo!”.
C++ non era originariamente un linguaggio con supporto della programmazione funzionale, ma, a partire dal C++11, ha introdotto diverse funzionalità che permettono di trattare le funzioni come valori di prima classe. Queste caratteristiche sono implementate tramite puntatori a funzione, oggetti funzione (std::function
) e espressioni lambda. 1
1 Per ulteriori informazioni, si può consultare la documentazione ufficiale di C++11.
Esempio:
#include <iostream>
#include <functional>
1void saluto(const std::string& nome) {
std::cout << "Ciao, " << nome << "!" << std::endl;
}
int main() {
2 std::function<void(const std::string&)> messaggio = saluto;
3 messaggio("Mondo");
// Passaggio come argomenti
4 auto chiamataDiRitorno = [](std::function<void(const std::string&)> f, const std::string& nome) {
f(nome);
};
5 chiamataDiRitorno(saluto, "Mondo");
return 0;
}
- 1
-
Definizione della funzione
saluto
che accetta un parametronome
. - 2
-
Assegnazione della funzione
saluto
all’oggetto funzionemessaggio
utilizzandostd::function
. - 3
-
Chiamata della funzione
messaggio
con l’argomento"Mondo"
, che stampa “Ciao, Mondo!”. - 4
-
Definizione di una lambda expression
chiamataDiRitorno
che accetta una funzionef
e un valorenome
. - 5
-
Chiamata della lambda
chiamataDiRitorno
con la funzionesaluto
e l’argomento"Mondo"
, che stampa “Ciao, Mondo!”.
6.6.2 La funzione di ordine superiore
La funzione di ordine superiore è una diretta conseguenza del supporto per le funzioni in prima classe. Tale funzione accetta altre funzioni come argomenti e/o ritornano funzioni come risultati. Nel paradigma della programmazione funzionale, le funzioni di ordine superiore facilitano l’implementazione di tecniche come la composizione di funzioni, l’applicazione parziale e l’uso di callback.
Esempio in Python:
def somma(a):
def inner(b):
return a + b
1 return inner
2aggiungi_cinque = somma(5)
3print(aggiungi_cinque(3))
- 1
-
La funzione
somma
ritorna una nuova funzioneinner
che sommaa
al suo argomentob
. - 2
-
somma(5)
ritorna una nuova funzione che somma5
al suo argomento. - 3
-
La funzione risultante viene chiamata con l’argomento
3
, restituendo8
.
Esempio in C++:
#include <iostream>
#include <functional>
1std::function<int(int)> somma(int a) {
2 return [a](int b) { return a + b; };
}
int main() {
3 auto aggiungi_cinque = somma(5);
4 std::cout << aggiungi_cinque(3) << std::endl;
return 0;
}
- 1
-
Dichiarazione della funzione
somma
che ritorna unstd::function<int(int)>
. - 2
-
somma
ritorna una funzione lambda che sommaa
al suo argomentob
. - 3
-
somma(5)
ritorna una nuova funzione che somma5
al suo argomento. - 4
-
La funzione risultante viene chiamata con l’argomento
3
, restituendo8
.
6.6.3 L’applicazione parziale
L’applicazione parziale (inglese: partial application) è una tecnica della programmazione funzionale che permette di fissare un certo numero di parametri di una funzione, producendo una nuova funzione con un numero inferiore dei medesimi. Ciò è particolarmente utile quando si desidera creare varianti di una funzione con alcuni parametri predefiniti, aumentando così la flessibilità e la riutilizzabilità del codice.
In pratica, l’applicazione parziale consente di preimpostare alcuni argomenti di una funzione, riducendo il numero di argomenti che devono essere forniti successivamente.
Python supporta l’applicazione parziale tramite il modulo functools
che include la funzione partial
:
- 1
- Funzione che accetta tre argomenti.
- 2
-
Utilizzo di
partial
per fissare il primo argomento dif
a1
. - 3
-
Chiamata della funzione
g
con i restanti due argomenti. Output:6
.
In C++, l’applicazione parziale può essere realizzata utilizzando le espressioni lambda o la funzione std::bind
dalla libreria standard:
#include <iostream>
#include <functional>
int somma(int a, int b, int c) {
return a + b + c;
}
int main() {
1 auto fissaPrimoArgomento = std::bind(somma, 1, std::placeholders::_1, std::placeholders::_2);
2 std::cout << fissaPrimoArgomento(2, 3) << std::endl;
return 0;
}
- 1
-
Utilizzo di
std::bind
per fissare il primo argomento disomma
a 1. - 2
-
Chiamata della funzione
fissaPrimoArgomento
con i restanti due argomenti. Output:6
.
L’applicazione parziale è importante perché:
Aumenta la modularità: Permette di creare versioni specifiche di funzioni generiche.
Riduce la ridondanza: Evita la necessità di riscrivere funzioni simili con parametri diversi.
Facilita la composizione: Supporta la creazione di funzioni più complesse a partire da funzioni più semplici.
L’applicazione parziale è una tecnica potente che, insieme alle funzioni di ordine superiore, contribuisce alla flessibilità e alla manutenibilità del codice nel paradigma della programmazione funzionale.
6.6.4 Il decoratore
Il decoratore è una potente funzionalità in Python che permette di modificare il comportamento di funzioni o metodi esistenti senza cambiarne il codice sorgente. Esso sfrutta i concetti di funzioni in prima classe e funzioni di ordine superiore per aggiungere nuove funzionalità in modo modulare e riutilizzabile. È necessaria una sintassi ad hoc per applicare i decoratori al nostro codice ma il processo risulta semplice e il prodotto molto leggibile.
Un decoratore è essenzialmente una funzione che accetta un’altra funzione come argomento e restituisce una nuova funzione. Questa nuova funzione può estendere o modificare il comportamento della funzione originale.
Questo consente di estendere le funzionalità di una funzione in modo modulare e senza alterare il suo codice originale, utile sia per creare librerie di modifiche a funzioni proprie, sia per aggiornare il codice mantenendo la compatibilità con le versioni precedenti.
Esempio generico:
1def mio_decoratore(f):
2 def involucro(*args, **kwargs):
3 print("Qualcosa prima della funzione")
4 risultato = f(*args, **kwargs)
5 print("Qualcosa dopo la funzione")
6 return risultato
7 return involucro
8@mio_decoratore
9def di_ciao():
10 print("Ciao!")
11di_ciao()
- 1
-
Definizione della funzione
mio_decoratore
, che accetta una funzionef
come argomento. - 2
-
Definizione della funzione
involucro
interna, che accetta argomenti variabili*args
e**kwargs
. - 3
- Stampa di un messaggio prima della chiamata della funzione decorata.
- 4
-
Chiamata della funzione originale
f
con gli argomenti originali. - 5
- Stampa di un messaggio dopo la chiamata della funzione decorata.
- 6
-
Restituzione del risultato della funzione
f
. - 7
-
Restituzione della funzione
involucro
come nuova funzione decorata. - 8
-
Applicazione del decoratore
mio_decoratore
alla funzionedi_ciao
. - 9
-
Definizione della funzione
di_ciao
. - 10
-
Stampa del messaggio
Ciao!
. - 11
-
Chiamata della funzione
di_ciao
decorata.
6.6.4.1 Applicazioni
I decoratori sono molto utili per creare librerie che estendono le funzionalità delle funzioni esistenti senza modificarne il codice sorgente. Ad esempio, si potrebbe creare un decoratore per registrare il tempo di esecuzione di una funzione:
import time
1def calcolo_tempo_esecuzione(f):
2 def involucro(*args, **kwargs):
3 start_time = time.time()
4 result = f(*args, **kwargs)
5 end_time = time.time()
6 print(f"Tempo di esecuzione: {end_time - start_time} secondi")
7 return result
8 return involucro
9@calcolo_tempo_esecuzione
10def esempio_funzione():
11 time.sleep(2)
print("Funzione eseguita")
12esempio_funzione()
- 1
-
Definizione del decoratore
calcolo_tempo_esecuzione
. - 2
-
Definizione della funzione
involucro
interna. - 3
- Registrazione del tempo di inizio.
- 4
-
Chiamata della funzione originale
f
. - 5
- Registrazione del tempo di fine.
- 6
- Stampa del tempo di esecuzione.
- 7
-
Restituzione del risultato della funzione
f
. - 8
-
Restituzione della funzione
involucro
come nuova funzione decorata. - 9
-
Applicazione del decoratore
calcolo_tempo_esecuzione
alla funzioneesempio_funzione
. - 10
-
Definizione della funzione
esempio_funzione
. - 11
- Simulazione di un ritardo di 2 secondi.
- 12
-
Chiamata della funzione
esempio_funzione
decorata.
Il seguente esempio mostra come utilizzare un decoratore per mantenere la compatibilità all’indietro di una funzione di cui è stato modificato l’elenco dei parametri. La nuova versione della funzione accetta un parametro aggiuntivo, ma il vecchio codice può continuare a chiamare la funzione senza passare questo parametro aggiuntivo.
Esempio:
1def compatibilita_indietro(f):
2 def involucro(*args, **kwargs):
try:
3 return f(*args, **kwargs)
4 except TypeError as e:
5 if "positional argument" in str(e):
6 return f(args[0])
7 raise e
8 return involucro
9@compatibilita_indietro
10def esempio_funzione(nome, messaggio="Ciao!"):
print(f"{messaggio} {nome}")
11esempio_funzione("Mondo", messaggio="Salve")
12esempio_funzione("Mondo")
- 1
-
Definizione del decoratore
compatibilita_indietro
. - 2
-
Definizione della funzione
involucro
interna. - 3
-
Tentativo di chiamare la funzione originale
f
con tutti gli argomenti. - 4
- Gestione del TypeError che potrebbe verificarsi se gli argomenti non sono corretti.
- 5
- Verifica se l’errore è dovuto a un numero errato di argomenti posizionali.
- 6
-
Chiamata della funzione originale
f
con solo il primo argomento (compatibilità all’indietro). - 7
- Se l’errore è diverso, viene rilanciato.
- 8
-
Restituzione della funzione
involucro
come nuova funzione decorata. - 9
-
Applicazione del decoratore
compatibilita_indietro
alla funzioneesempio_funzione
. - 10
-
Definizione della funzione modificata
esempio_funzione
con un parametro aggiuntivomessaggio
. - 11
-
Chiamata della nuova funzione nel nuovo modo, con entrambi i parametri. Output:
Salve Mondo
. - 12
-
Chiamata della nuova funzione nel vecchio modo, con un solo parametro. Output:
Ciao! Mondo
.
Questo approccio garantisce che il nuovo codice possa utilizzare la nuova funzionalità, mentre il vecchio codice continua a funzionare senza modifiche.
6.6.4.2 Supporto in Typescript
TypeScript supporta, allo stato in modo sperimentale, i decoratori per classi, metodi, accessori, proprietà e parametri. Di seguito è riportato un esempio di decoratore per una classe:
1function logCostruzione(target: Function) {
2 console.log(`Costruzione di ${target.name}`);
}
3@logCostruzione
4class Persona {
5 constructor(public nome: string) {
6 console.log(`Ciao, ${nome}!`);
}
}
7const p = new Persona('Alice');
- 1
-
Definizione del decoratore
logCostruzione
. - 2
- Il decoratore stampa un messaggio con il nome della classe.
- 3
-
Applicazione del decoratore
logCostruzione
alla classePersona
. - 4
-
Definizione della classe
Persona
. - 5
-
Costruttore della classe
Persona
che accetta un parametronome
. - 6
- Il costruttore stampa un messaggio di saluto.
- 7
-
Creazione di un’istanza della classe
Persona
.
6.6.5 La funzione di richiamo
La funzione di richiamo (inglese: callback function o semplicemente callback) è un tipo di funzione che viene passata come argomento ad altra funzione e viene eseguita dopo che l’operazione principale sia terminata. Le funzioni di richiamo sono utilizzate in molti linguaggi di programmazione, inclusi Python, JavaScript, C++, e altri e sono particolarmente comuni nella programmazione asincrona, come la gestione di eventi e la programmazione basata su temporizzatori.
Ambiti di applicazione:
Programmazione asincrona: Utilizzate per gestire operazioni che richiedono tempo, come richieste di rete, lettura/scrittura su file, e interazioni con database.
Gestione degli eventi: Utilizzate in interfacce grafiche e applicazioni web per rispondere a eventi come click di pulsanti, input da tastiera, e movimenti del mouse.
Manipolazione di dati: Utilizzate per eseguire operazioni su dati in strutture come array o liste, ad esempio in Python con funzioni come
map
,filter
ereduce
2.
2 Le funzioni map
, filter
e reduce
sono strumenti fondamentali nella programmazione funzionale, utilizzati per operare su collezioni di dati in modo dichiarativo e conciso.
map
: La funzionemap
applica una funzione a ogni elemento di una collezione (come una lista o un array) e restituisce una nuova collezione contenente i risultati. È utile per trasformare o manipolare i dati di una collezione senza utilizzare esplicitamente cicli.filter
: La funzionefilter
prende una funzione di predicato (una funzione che restituisce un valore booleano) e una collezione. Restituisce una nuova collezione contenente solo gli elementi che soddisfano il predicato. È utile per selezionare o filtrare elementi specifici da una collezione in base a una condizione.reduce
: La funzionereduce
applica una funzione di aggregazione (una funzione che combina due elementi in uno) a una collezione, riducendola a un singolo valore. È utile per calcolare valori cumulativi, come la somma, il prodotto o altre operazioni di aggregazione su una collezione di dati.
Per approndire si può fare riferimento a (Wikipedia contrib. 2024a), (Wikipedia contrib. 2024b), (Wikipedia contrib. 2024c), (Abelson, Jay Sussman 1996).
Esempio generico in Python:
1def chiamata_di_ritorno(f):
2 print("Prima della callback")
3 f()
4 print("Dopo la callback")
5def saluto():
6 print("Ciao!")
7chiamata_di_ritorno(saluto)
- 1
-
Definizione della funzione
chiamata_di_ritorno
che accetta una funzione di richiamof
. - 2
-
Stampa di un messaggio prima dell’esecuzione della funzione
f
. - 3
-
Chiamata della funzione
f
. - 4
- Stampa di un messaggio dopo la callback.
- 5
-
Definizione della funzione
saluto
. - 6
-
Stampa del messaggio
Ciao!
. - 7
-
Passaggio della funzione
saluto
come funzione di richiamo achiamata_di_ritorno
.
Esempio pratico in Python]:
numeri = [1, 2, 3, 4, 5]
1quadrati = map(lambda x: x ** 2, numeri)
2print(list(quadrati))
numeri = [1, 2, 3, 4, 5]
3pari = filter(lambda x: x % 2 == 0, numeri)
4print(list(pari))
from functools import reduce
numeri = [1, 2, 3, 4, 5]
5somma = reduce(lambda x, y: x + y, numeri)
6print(somma)
- 1
-
Uso di
map
con una funzione lambda per calcolare i quadrati dei numeri. - 2
-
Conversione dell’oggetto
map
in una lista e stampa del risultato[1, 4, 9, 16, 25]
. - 3
-
Uso di
filter
con una funzione lambda per selezionare i numeri pari. - 4
-
Conversione dell’oggetto
filter
in una lista e stampa del risultato[2, 4]
. - 5
-
Uso di
reduce
con una funzione lambda per sommare tutti i numeri. - 6
-
Stampa del risultato
15
.
6.6.6 La lambda
Le lambda sono funzioni anonime che possono essere definite in una singola riga di codice. Sono utili per operazioni semplici e brevi. Le lambda sono supportate da molti linguaggi di programmazione, come Python, JavaScript, Java, C#, e altri, e forniscono un modo conciso per definire funzioni temporanee o usa e getta[^3-prima-parte-variabili-funzioni].
[^3-prima-parte-variabili-funzioni] Le espressioni lambda sono ispirate al calcolo lambda, una notazione matematica introdotta da Alonzo Church negli anni ’30. Il calcolo lambda è un sistema formale per esprimere computazioni basate sulla definizione e applicazione di funzioni anonime. Questo concetto è alla base delle lambda in molti linguaggi di programmazione moderni, facilitando l’adozione del paradigma della programmazione funzionale. Vedi anche (Wikipedia contrib. 2024d) e (Church 1936).
Una sintassi ad hoc per le lambda è necessaria per mantenere il codice leggibile e per permettere l’uso di funzioni anonime in modo rapido e senza definizioni formali che potrebbero rendere il codice più verboso e meno chiaro.
6.6.6.1 Python
Python utilizza la parola chiave lambda
per definire funzioni anonime. La sintassi è:
Esempio:
- 1
-
La funzione lambda
lambda a, b: a + b
somma due numeri. Qui,somma
è l’identificatore della lambda. - 2
-
Chiamata della funzione lambda con argomenti
3
e4
, stampa7
.
6.6.6.2 JavaScript
JavaScript utilizza le funzioni freccia (inglese: arrow functions) per definire funzioni anonime. La sintassi è:
Esempio:
- 1
- Definizione di una funzione lambda che somma due numeri.
- 2
-
Chiamata della funzione lambda con argomenti
3
e4
, stampa7
.
6.6.6.3 Java
Java utilizza le espressioni lambda, introdotte con Java 8. La sintassi è:
Esempio:
import java.util.function.*;
public class Main {
public static void main(String[] args) {
1 BiFunction<Integer, Integer, Integer> somma = (a, b) -> a + b;
2 System.out.println(somma.apply(3, 4));
}
}
- 1
-
Qui definiamo una lambda che somma due numeri. Utilizziamo l’interfaccia funzionale
BiFunction
per rappresentare una funzione che accetta due argomenti di tipoInteger
e restituisce un risultato di tipoInteger
. L’assegnazione(a, b) -> a + b
definisce la funzione lambda. - 2
-
Utilizziamo il metodo
apply
dellaBiFunction
per chiamare la funzione lambda con gli argomenti3
e4
, e stampiamo il risultato, che è7
.
6.6.7 Chiusure
Le chiusure (inglese: closure) sono funzioni che ricordano l’ambiente nel quale sono state create. Questo significa che possono accedere alle variabili definite nell’ambiente esterno anche dopo che tale ambiente sia stato chiuso.
Esempio in Python:
1def crea_sommatore(x):
2 def somma(y):
3 return x + y
4 return somma
5aggiungi_cinque = crea_sommatore(5)
6print(aggiungi_cinque(3))
- 1
-
Definizione della funzione
crea_sommatore
che accetta un parametrox
. - 2
-
Definizione della funzione
somma
che accetta un parametroy
. - 3
-
La funzione
somma
sommax
ey
. - 4
-
crea_sommatore
restituisce la funzionesomma
. - 5
-
aggiungi_cinque
è una chiusura che ricorda il valore dix
come5
. - 6
-
aggiungi_cinque(3)
restituisce8
.
Esempio in JavaScript:
1function creaSommatore(x) {
2 return function(y) {
3 return x + y;
};
}
4const aggiungiCinque = creaSommatore(5);
5console.log(aggiungiCinque(3));
- 1
-
Definizione della funzione
creaSommatore
che accetta un parametrox
. - 2
-
Restituzione di una funzione che accetta un parametro
y
. - 3
-
La funzione interna somma
x
ey
. - 4
-
aggiungiCinque
è una chiusura che ricorda il valore dix
come5
. - 5
-
aggiungiCinque(3)
restituisce8
.
Le chiusure sono utili in diversi contesti, tra cui:
Memorizzazione di stato: Le chiusure possono essere utilizzate per mantenere uno stato tra chiamate successive a una funzione. Esempio in Python:
1def crea_contatore(): 2 conto = 0 3 def contatore(): 4 nonlocal conto 5 conto += 1 6 return conto 7 return contatore 8contatore = crea_contatore() 9print(contatore()) 10print(contatore())
- 1
-
Definizione della funzione
crea_contatore
. - 2
-
Inizializzazione della variabile
conto
a0
. - 3
-
Definizione della funzione
contatore
. - 4
-
Dichiarazione della variabile
conto
comenonlocal
perché siua modificabile all’interno dicontatore
. - 5
-
Incremento della variabile
conto
. - 6
-
Restituzione del valore di
conto
. - 7
-
Restituzione della funzione
contatore
. - 8
-
Creazione della chiusura
contatore
. - 9
-
Prima chiamata di
contatore()
, restituisce1
. - 10
-
Seconda chiamata di
contatore()
, restituisce2
.
Funzioni factory: Permettono la creazione di funzioni personalizzate configurate con parametri specifici. Esempio in Python:
1def moltiplica_per(fattore): 2 def moltiplica(numero): 3 return numero * fattore 4 return moltiplica 5double = moltiplica_per(2) 6print(double(5))
- 1
-
Definizione della funzione
moltiplica_per
che accetta un parametrofattore
. - 2
-
Definizione della funzione
moltiplica
che accetta un parametronumero
. - 3
-
La funzione
moltiplica
moltiplicanumero
perfattore
. - 4
-
moltiplica_per
restituisce la funzionemoltiplica
. - 5
-
double
è una chiusura che ricorda il valore difattore
come2
. - 6
-
double(5)
restituisce10
.
Funzioni di richiamo e gestione degli eventi: In programmazione asincrona, le chiusure sono spesso utilizzate per definire funzioni di richiamo che ricordano il contesto in cui sono state create. Esempio in Python:
1def on_event(message): 2 def handle_event(): 3 print(f"Event: {message}") 4 return handle_event 5event_handler = on_event("Hello World") 6event_handler()
- 1
-
Definizione della funzione
on_event
che accetta un parametromessage
. - 2
-
Definizione della funzione
handle_event
. - 3
-
La funzione
handle_event
stampamessage
. - 4
-
on_event
restituisce la funzionehandle_event
. - 5
-
event_handler
è una chiusura che ricorda il valore dimessage
comeHello World
. - 6
-
Chiamata della funzione
event_handler
, stampaEvent: Hello World
.
Programmazione funzionale: Le chiusure sono un costrutto fondamentale per molte tecniche della programmazione funzionale, come l’applicazione parziale e la trasformazione di una funzione con parametri multipli in una sequenza di funzioni aventi un unico parametro (inglese: currying).3
Il currying può essere estremamente utile per creare funzioni generiche che possono essere specializzate in vari contesti. Ad esempio, consideriamo un’applicazione che richiede l’invio di messaggi a diversi destinatari con diverse priorità.
Esempio in Python:
1def invia_messaggio(tipo_messaggio): 2 def messaggio(priorita, destinatario, testo): 3 if tipo_messaggio == "Email": 4 print(f"Invio email a {destinatario} con priorità {priorita}: {testo}") 5 elif tipo_messaggio == "SMS": 6 print(f"Invio SMS a {destinatario} con priorità {priorita}: {testo}") 7 return messaggio 8invia_email = invia_messaggio("Email") 9invia_sms = invia_messaggio("SMS") 10invia_email("Alta", "alice@example.com", "Ciao Alice!") 11invia_sms("Bassa", "1234567890", "Ciao!")
- 1
-
Definizione della funzione
invia_messaggio
che accetta un parametrotipo_messaggio
. - 2
-
Definizione della funzione
messaggio
che accettapriorita
,destinatario
, etesto
. - 3
- Controllo del tipo di messaggio.
- 4
- Stampa specifica per l’invio di un’email.
- 5
- Controllo del tipo di messaggio.
- 6
- Stampa specifica per l’invio di un SMS.
- 7
-
invia_messaggio
restituisce la funzionemessaggio
. - 8
-
invia_email
è una funzione specializzata per inviare email. - 9
-
invia_sms
è una funzione specializzata per inviare SMS. - 10
- Invio di un’email con priorità alta.
- 11
- Invio di un SMS con priorità bassa.
Esempio in Haskell:
-- Definizione della funzione curryata per inviare messaggi 1inviaMessaggio :: String -> String -> String -> String -> IO () inviaMessaggio tipoMessaggio priorita destinatario testo = 2 if tipoMessaggio == "Email" 3 then putStrLn ("Invio email a " ++ destinatario ++ " con priorità " ++ priorita ++ ": " ++ testo) 4 else if tipoMessaggio == "SMS" 5 then putStrLn ("Invio SMS a " ++ destinatario ++ " con priorità " ++ priorita ++ ": " ++ testo) 6 else putStrLn "Tipo di messaggio sconosciuto" -- Funzioni specializzate 7inviaEmail :: String -> String -> String -> IO () 8inviaEmail = inviaMessaggio "Email" 9inviaSMS :: String -> String -> String -> IO () 10inviaSMS = inviaMessaggio "SMS" 11main :: IO () main = do 12 inviaEmail "Alta" "alice@example.com" "Ciao Alice!" 13 inviaSMS "Bassa" "1234567890" "Ciao!"
- 1
-
Definizione della funzione
inviaMessaggio
che accetta quattro parametri: tipo di messaggio, priorità, destinatario e testo. - 2
- Controllo del tipo di messaggio.
- 3
- Stampa specifica per l’invio di un’email.
- 4
- Controllo del tipo di messaggio.
- 5
- Stampa specifica per l’invio di un SMS.
- 6
- Gestione di un tipo di messaggio sconosciuto.
- 7
-
Definizione del tipo della funzione
inviaEmail
. - 8
-
inviaEmail
è una funzione specializzata per inviare email. - 9
-
Definizione del tipo della funzione
inviaSMS
. - 10
-
inviaSMS
è una funzione specializzata per inviare SMS. - 11
-
Definizione della funzione
main
. - 12
- Invio di un’email con priorità alta.
- 13
- Invio di un SMS con priorità bassa.
3 Il currying è una tecnica di trasformazione delle funzioni che prende il nome dal logico matematico Haskell Curry, sebbene il concetto sia stato inizialmente sviluppato da Moses Schönfinkel. In contesto matematico, questo principio può essere fatto risalire ai lavori di Gottlob Frege del 1893. Il currying consiste nel trasformare una funzione con più argomenti in una sequenza di funzioni ciascuna delle quali accetta un singolo argomento. Formalmente, data una funzione \(f\) di due variabili \(f(x, y)\), il currying la trasforma in una funzione \(g\) tale che \(g(x)(y)=f(x,y)\). Il linguaggio di programmazione Haskell, che supporta nativamente il currying, è stato chiamato così in onore proprio di Haskell Curry, riconoscendo il suo contributo alla logica combinatoria e alla teoria delle funzioni. Per approfondire: (Wikipedia contrib. 2024e), (Haskell wiki contrib. 2024), (Abelson, Jay Sussman 1996), (Curry 1950).