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:

1def somma(a, b):
2  return a + b
1
Definizione della funzione somma con due parametri a e b.
2
La funzione somma ritorna la somma dei parametri a e b.

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:

1def somma(a, b):
  return a + b

2result = somma(3, 4)

3print(result)
1
a e b sono parametri della funzione somma.
2
3 e 4 sono argomenti passati alla funzione somma.
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 di a e b.
3
Chiamata della funzione somma con argomenti 3 e 4.
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 di main.

    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 di funzione_esterna.
    2
    Chiamata della funzione funzione_locale all’interno di funzione_esterna.
    3
    Chiamata a funzione_locale al di fuori di funzione_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 di funzione_esterna.
    2
    Chiamata della funzione funzione_supporto all’interno di funzione_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 usando delete. 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 con n - 1.
4
Chiamata della funzione fattoriale con argomento 5.
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 parametro nome.
2
Assegnazione della funzione saluto alla variabile messaggio.
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 parametro f.
5
Chiamata della funzione chiamata_di_ritorno con la funzione saluto come argomento, che stampa “Ciao, Mondo!”.
6
Definizione della funzione crea_saluto che restituisce una funzione.
7
Definizione della funzione saluto interna a crea_saluto.
8
Restituzione della funzione saluto da crea_saluto.
9
Assegnazione della funzione restituita da crea_saluto alla variabile saluta.
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 parametro nome.
2
Assegnazione della funzione saluto alla variabile messaggio.
3
Chiamata della funzione messaggio con l’argomento "Mondo", che stampa “Ciao, Mondo!”.
4
Definizione della funzione chiamataDiRitorno che accetta una funzione come parametro f.
5
Chiamata della funzione chiamataDiRitorno con la funzione saluto come argomento, che stampa “Ciao, Mondo!”.
6
Definizione della funzione creaSaluto che restituisce una funzione.
7
Definizione della funzione saluto interna a creaSaluto.
8
Restituzione della funzione saluto da creaSaluto.
9
Assegnazione della funzione restituita da creaSaluto alla variabile saluta.
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 stringa nome e restituisce una stringa.
2
Assegnazione della funzione saluto alla variabile messaggio.
3
Chiamata della funzione messaggio con l’argomento "Mondo", che stampa “Ciao, Mondo!”.
4
Definizione della funzione chiamataDiRitorno che accetta una funzione come parametro f.
5
Chiamata della funzione chiamataDiRitorno con la funzione saluto come argomento, che stampa “Ciao, Mondo!”.
6
Definizione della funzione creaSaluto che restituisce la funzione saluto.
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 parametro nome.
2
Assegnazione della funzione saluto all’oggetto funzione messaggio utilizzando std::function.
3
Chiamata della funzione messaggio con l’argomento "Mondo", che stampa “Ciao, Mondo!”.
4
Definizione di una lambda expression chiamataDiRitorno che accetta una funzione f e un valore nome.
5
Chiamata della lambda chiamataDiRitorno con la funzione saluto 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 funzione inner che somma a al suo argomento b.
2
somma(5) ritorna una nuova funzione che somma 5 al suo argomento.
3
La funzione risultante viene chiamata con l’argomento 3, restituendo 8.

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 un std::function<int(int)>.
2
somma ritorna una funzione lambda che somma a al suo argomento b.
3
somma(5) ritorna una nuova funzione che somma 5 al suo argomento.
4
La funzione risultante viene chiamata con l’argomento 3, restituendo 8.

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:

from functools import partial

1def f(a, b, c):
  return a + b + c

2g = partial(f, 1)

3print(g(2, 3))
1
Funzione che accetta tre argomenti.
2
Utilizzo di partial per fissare il primo argomento di f a 1.
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 di somma 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 funzione f 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 funzione di_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 funzione esempio_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 funzione esempio_funzione.
10
Definizione della funzione modificata esempio_funzione con un parametro aggiuntivo messaggio.
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 classe Persona.
4
Definizione della classe Persona.
5
Costruttore della classe Persona che accetta un parametro nome.
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 e reduce2.

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 funzione map 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 funzione filter 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 funzione reduce 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).

WIKIPEDIA CONTRIB., 2024a. Map (higher-order function) [online]. 2024. Disponibile all'indirizzo : https://en.wikipedia.org/wiki/Map_(higher-order_function)
Accessed: 2024-07-09
WIKIPEDIA CONTRIB., 2024b. Filter (higher-order function) [online]. 2024. Disponibile all'indirizzo : https://en.wikipedia.org/wiki/Filter_(higher-order_function)
Accessed: 2024-07-09
WIKIPEDIA CONTRIB., 2024c. Reduce (higher-order function) [online]. 2024. Disponibile all'indirizzo : https://en.wikipedia.org/wiki/Reduce_(higher-order_function)
Accessed: 2024-07-09

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 richiamo f.
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 a chiamata_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).

WIKIPEDIA CONTRIB., 2024d. Lambda calculus [online]. 2024. Disponibile all'indirizzo : https://en.wikipedia.org/wiki/Lambda_calculus
Accessed: 2024-07-09
CHURCH, Alonzo, 1936. An Unsolvable Problem of Elementary Number Theory. American Journal of Mathematics. 1936. Vol. 58, n° 2, pp. 345–363. DOI 10.2307/2371045.

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 è:

lambda parametri: espressione

Esempio:

1somma = lambda a, b: a + b

2print(somma(3, 4))
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 e 4, stampa 7.

6.6.6.2 JavaScript

JavaScript utilizza le funzioni freccia (inglese: arrow functions) per definire funzioni anonime. La sintassi è:

(parametri) => espressione

Esempio:

1const somma = (a, b) => a + b;

2console.log(somma(3, 4));
1
Definizione di una funzione lambda che somma due numeri.
2
Chiamata della funzione lambda con argomenti 3 e 4, stampa 7.

6.6.6.3 Java

Java utilizza le espressioni lambda, introdotte con Java 8. La sintassi è:

(parametri) -> espressione

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 tipo Integer e restituisce un risultato di tipo Integer. L’assegnazione (a, b) -> a + b definisce la funzione lambda.
2
Utilizziamo il metodo apply della BiFunction per chiamare la funzione lambda con gli argomenti 3 e 4, 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 parametro x.
2
Definizione della funzione somma che accetta un parametro y.
3
La funzione somma somma x e y.
4
crea_sommatore restituisce la funzione somma.
5
aggiungi_cinque è una chiusura che ricorda il valore di x come 5.
6
aggiungi_cinque(3) restituisce 8.

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 parametro x.
2
Restituzione di una funzione che accetta un parametro y.
3
La funzione interna somma x e y.
4
aggiungiCinque è una chiusura che ricorda il valore di x come 5.
5
aggiungiCinque(3) restituisce 8.

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 a 0.
    3
    Definizione della funzione contatore.
    4
    Dichiarazione della variabile conto come nonlocal perché siua modificabile all’interno di contatore.
    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(), restituisce 1.
    10
    Seconda chiamata di contatore(), restituisce 2.
  • 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 parametro fattore.
    2
    Definizione della funzione moltiplica che accetta un parametro numero.
    3
    La funzione moltiplica moltiplica numero per fattore.
    4
    moltiplica_per restituisce la funzione moltiplica.
    5
    double è una chiusura che ricorda il valore di fattore come 2.
    6
    double(5) restituisce 10.
  • 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 parametro message.
    2
    Definizione della funzione handle_event.
    3
    La funzione handle_event stampa message.
    4
    on_event restituisce la funzione handle_event.
    5
    event_handler è una chiusura che ricorda il valore di message come Hello World.
    6
    Chiamata della funzione event_handler, stampa Event: 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 parametro tipo_messaggio.
    2
    Definizione della funzione messaggio che accetta priorita, destinatario, e testo.
    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 funzione messaggio.
    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).

WIKIPEDIA CONTRIB., 2024e. Currying [online]. 2024. Disponibile all'indirizzo : https://en.wikipedia.org/wiki/Currying
Accessed: 2024-07-09
HASKELL WIKI CONTRIB., 2024. Currying [online]. 2024. Disponibile all'indirizzo : https://wiki.haskell.org/Currying
Accessed: 2024-07-09
ABELSON, Harold e JAY SUSSMAN, Gerald, 1996. Structure and Interpretation of Computer Programs. 2nd. MIT Press. ISBN 978-0262510875.
CURRY, Haskell B., 1950. A Theory of Formal Deducibility. University of Notre Dame Press. Notre Dame Mathematical Lectures, 6.