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
sommacon due parametriaeb. - 2
-
La funzione
sommaritorna la somma dei parametriaeb.
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
-
aebsono parametri della funzionesomma. - 2
-
3e4sono argomenti passati alla funzionesomma. - 3
-
Il risultato della funzione
sommaviene 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
sommache accetta due parametri interi. - 2
-
La funzione
sommaritorna la somma diaeb. - 3
-
Chiamata della funzione
sommacon argomenti3e4. - 4
-
Il risultato della funzione
sommaviene 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
funzioneGlobalea livello globale. - 2
-
Chiamata della funzione
funzioneGlobaleall’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_localeall’interno difunzione_esterna. - 2
-
Chiamata della funzione
funzione_localeall’interno difunzione_esterna. - 3
-
Chiamata a
funzione_localeal di fuori difunzione_esterna, che genera un errore poichéfunzione_localenon è 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_supportoall’interno difunzione_esterna. - 2
-
Chiamata della funzione
funzione_supportoall’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
newe 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:
fattorialechiama se stessa conn - 1. - 4
-
Chiamata della funzione
fattorialecon argomento5. - 5
-
Il risultato della funzione
fattorialeviene 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
salutoche accetta un parametronome. - 2
-
Assegnazione della funzione
salutoalla variabilemessaggio. - 3
-
Chiamata della funzione
messaggiocon l’argomento"Mondo", che stampa “Ciao, Mondo!”. - 4
-
Definizione della funzione
chiamata_di_ritornoche accetta una funzione come parametrof. - 5
-
Chiamata della funzione
chiamata_di_ritornocon la funzionesalutocome argomento, che stampa “Ciao, Mondo!”. - 6
-
Definizione della funzione
crea_salutoche restituisce una funzione. - 7
-
Definizione della funzione
salutointerna acrea_saluto. - 8
-
Restituzione della funzione
salutodacrea_saluto. - 9
-
Assegnazione della funzione restituita da
crea_salutoalla variabilesaluta. - 10
-
Chiamata della funzione
salutacon 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
salutoche accetta un parametronome. - 2
-
Assegnazione della funzione
salutoalla variabilemessaggio. - 3
-
Chiamata della funzione
messaggiocon l’argomento"Mondo", che stampa “Ciao, Mondo!”. - 4
-
Definizione della funzione
chiamataDiRitornoche accetta una funzione come parametrof. - 5
-
Chiamata della funzione
chiamataDiRitornocon la funzionesalutocome argomento, che stampa “Ciao, Mondo!”. - 6
-
Definizione della funzione
creaSalutoche restituisce una funzione. - 7
-
Definizione della funzione
salutointerna acreaSaluto. - 8
-
Restituzione della funzione
salutodacreaSaluto. - 9
-
Assegnazione della funzione restituita da
creaSalutoalla variabilesaluta. - 10
-
Chiamata della funzione
salutacon 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
salutoche accetta una stringanomee restituisce una stringa. - 2
-
Assegnazione della funzione
salutoalla variabilemessaggio. - 3
-
Chiamata della funzione
messaggiocon l’argomento"Mondo", che stampa “Ciao, Mondo!”. - 4
-
Definizione della funzione
chiamataDiRitornoche accetta una funzione come parametrof. - 5
-
Chiamata della funzione
chiamataDiRitornocon la funzionesalutocome argomento, che stampa “Ciao, Mondo!”. - 6
-
Definizione della funzione
creaSalutoche restituisce la funzionesaluto. - 7
-
Chiamata della funzione
creaSalutocon 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
salutoche accetta un parametronome. - 2
-
Assegnazione della funzione
salutoall’oggetto funzionemessaggioutilizzandostd::function. - 3
-
Chiamata della funzione
messaggiocon l’argomento"Mondo", che stampa “Ciao, Mondo!”. - 4
-
Definizione di una lambda expression
chiamataDiRitornoche accetta una funzionefe un valorenome. - 5
-
Chiamata della lambda
chiamataDiRitornocon la funzionesalutoe 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
sommaritorna una nuova funzioneinnerche sommaaal suo argomentob. - 2
-
somma(5)ritorna una nuova funzione che somma5al 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
sommache ritorna unstd::function<int(int)>. - 2
-
sommaritorna una funzione lambda che sommaaal suo argomentob. - 3
-
somma(5)ritorna una nuova funzione che somma5al 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
partialper fissare il primo argomento difa1. - 3
-
Chiamata della funzione
gcon 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::bindper fissare il primo argomento disommaa 1. - 2
-
Chiamata della funzione
fissaPrimoArgomentocon 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 funzionefcome argomento. - 2
-
Definizione della funzione
involucrointerna, che accetta argomenti variabili*argse**kwargs. - 3
- Stampa di un messaggio prima della chiamata della funzione decorata.
- 4
-
Chiamata della funzione originale
fcon 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
involucrocome nuova funzione decorata. - 8
-
Applicazione del decoratore
mio_decoratorealla funzionedi_ciao. - 9
-
Definizione della funzione
di_ciao. - 10
-
Stampa del messaggio
Ciao!. - 11
-
Chiamata della funzione
di_ciaodecorata.
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
involucrointerna. - 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
involucrocome nuova funzione decorata. - 9
-
Applicazione del decoratore
calcolo_tempo_esecuzionealla funzioneesempio_funzione. - 10
-
Definizione della funzione
esempio_funzione. - 11
- Simulazione di un ritardo di 2 secondi.
- 12
-
Chiamata della funzione
esempio_funzionedecorata.
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
involucrointerna. - 3
-
Tentativo di chiamare la funzione originale
fcon 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
fcon solo il primo argomento (compatibilità all’indietro). - 7
- Se l’errore è diverso, viene rilanciato.
- 8
-
Restituzione della funzione
involucrocome nuova funzione decorata. - 9
-
Applicazione del decoratore
compatibilita_indietroalla funzioneesempio_funzione. - 10
-
Definizione della funzione modificata
esempio_funzionecon 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
logCostruzionealla classePersona. - 4
-
Definizione della classe
Persona. - 5
-
Costruttore della classe
Personache 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,filterereduce2.
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 funzionemapapplica 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 funzionefilterprende 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 funzionereduceapplica 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_ritornoche 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
salutocome 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
mapcon una funzione lambda per calcolare i quadrati dei numeri. - 2
-
Conversione dell’oggetto
mapin una lista e stampa del risultato[1, 4, 9, 16, 25]. - 3
-
Uso di
filtercon una funzione lambda per selezionare i numeri pari. - 4
-
Conversione dell’oggetto
filterin una lista e stampa del risultato[2, 4]. - 5
-
Uso di
reducecon 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 + bsomma due numeri. Qui,sommaè l’identificatore della lambda. - 2
-
Chiamata della funzione lambda con argomenti
3e4, 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
3e4, 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
BiFunctionper rappresentare una funzione che accetta due argomenti di tipoIntegere restituisce un risultato di tipoInteger. L’assegnazione(a, b) -> a + bdefinisce la funzione lambda. - 2
-
Utilizziamo il metodo
applydellaBiFunctionper chiamare la funzione lambda con gli argomenti3e4, 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_sommatoreche accetta un parametrox. - 2
-
Definizione della funzione
sommache accetta un parametroy. - 3
-
La funzione
sommasommaxey. - 4
-
crea_sommatorerestituisce la funzionesomma. - 5
-
aggiungi_cinqueè una chiusura che ricorda il valore dixcome5. - 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
creaSommatoreche accetta un parametrox. - 2
-
Restituzione di una funzione che accetta un parametro
y. - 3
-
La funzione interna somma
xey. - 4
-
aggiungiCinqueè una chiusura che ricorda il valore dixcome5. - 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
contoa0. - 3
-
Definizione della funzione
contatore. - 4
-
Dichiarazione della variabile
contocomenonlocalperché 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_perche accetta un parametrofattore. - 2
-
Definizione della funzione
moltiplicache accetta un parametronumero. - 3
-
La funzione
moltiplicamoltiplicanumeroperfattore. - 4
-
moltiplica_perrestituisce la funzionemoltiplica. - 5
-
doubleè una chiusura che ricorda il valore difattorecome2. - 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_eventche accetta un parametromessage. - 2
-
Definizione della funzione
handle_event. - 3
-
La funzione
handle_eventstampamessage. - 4
-
on_eventrestituisce la funzionehandle_event. - 5
-
event_handlerè una chiusura che ricorda il valore dimessagecomeHello 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_messaggioche accetta un parametrotipo_messaggio. - 2
-
Definizione della funzione
messaggioche 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_messaggiorestituisce 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
inviaMessaggioche 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).