Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 1

Pure Functions vs. Side Effects

Recommended resources

Benvenuto al corso! Sono il tuo tutor AI e ti guiderò attraverso i paradigmi di programmazione avanzati e i design pattern.

Il nostro viaggio inizia con la programmazione funzionale (FP), un paradigma che sta guadagnando sempre più importanza nello sviluppo software moderno. Dato il tuo solido background nella programmazione imperativa con linguaggi come Python, Cobol e Perl, il nostro primo obiettivo sarà quello di evidenziare un cambiamento fondamentale nel modo di pensare: passeremo dal dare al computer una sequenza di comandi che modificano lo stato (control-flow) al descrivere le trasformazioni dei dati (data-flow).

In questa prima lezione, affronteremo il concetto più fondamentale della programmazione funzionale. L'obiettivo di oggi è: definire e identificare le funzioni pure rispetto alle funzioni con effetti collaterali (side effects). Comprendere questa distinzione è il primo, cruciale passo per padroneggiare il pensiero funzionale.

1. Cosa sono le Funzioni Pure e gli Effetti Collaterali?

Per iniziare, vediamo una definizione chiara e concisa di questi concetti. Una funzione pura è come una funzione matematica: è prevedibile e isolata dal resto del mondo.

Per una spiegazione introduttiva, guarda i primi minuti del seguente video. Si concentra su due idee principali:

  1. Una funzione pura, per lo stesso input, produce sempre lo stesso output.
  2. Una funzione pura non ha interazioni osservabili con il mondo esterno (i cosiddetti "side effects").

What are "Pure Functions" and "Side Effects"?

Il video "What are 'Pure Functions' and 'Side Effects'?" del canale Academind fornisce un'eccellente introduzione a questi concetti. Concentrati sulla definizione di base e sugli esempi iniziali.

Guarda la parte del video che va dall'inizio fino al timestamp 03:29. Presta attenzione ai due criteri che definiscono una funzione pura e a come l'esempio con console.log illustri un effetto collaterale.

Come hai visto, le due regole per una funzione pura sono semplici ma rigorose:

  • Determinismo: L'output dipende esclusivamente dai suoi argomenti di input.
  • Nessun effetto collaterale: La funzione non modifica alcuno stato esterno (variabili globali, file system, database) né ha interazioni osservabili con l'esterno (chiamate di rete, output sulla console).

L'immagine seguente riassume visivamente questa idea.

Characteristics of a Pure Function
Questa immagine illustra il concetto di funzione pura: per lo stesso input, restituisce sempre lo stesso output, senza dipendere o influenzare variabili esterne o produrre effetti casuali.

2. Esempi in Python

Vediamo come si traducono questi concetti in Python. L'articolo seguente mostra esempi chiari di funzioni pure e impure.

Functional Programming in Python - Ada Beat

L'articolo "Functional Programming in Python" di Ada Beat approfondisce la definizione di funzione pura con esempi pratici in Python.

Leggi la sezione intitolata "Pure Functions in Python". Concentrati sugli esempi di codice per la funzione square (pura) e add_to_result (impura), notando la spiegazione del perché la seconda non è pura.

La funzione add_to_result è un classico esempio di funzione impura perché viola entrambe le regole:

  1. Il suo output (implicito, attraverso la modifica dello stato globale) dipende dallo stato precedente della variabile result. Chiamandola più volte con lo stesso input 5 si ottengono risultati diversi per lo stato globale (5, 10, 15, ...).
  2. Ha un effetto collaterale: modifica la variabile globale result, che esiste al di fuori del suo scope.

Questo comportamento introduce imprevedibilità, rendendo il codice più difficile da testare, da eseguire in parallelo e da analizzare.


3. Il Cuore del Pensiero Funzionale: Data-Flow vs. Control-Flow

La distinzione tra puro e impuro non è solo una classificazione accademica; rappresenta un cambiamento radicale nel modo in cui strutturiamo i programmi. La programmazione imperativa, a cui sei abituato, si concentra sul control-flow: una sequenza di istruzioni che manipolano uno stato condiviso. La programmazione funzionale si concentra sul data-flow: come i dati fluiscono attraverso una catena di funzioni pure che li trasformano.

L'articolo "What's Functional Programming All About?" di Li Haoyi illustra magnificamente questa differenza usando un'analogia con una ricetta di Tiramisù. Ti guiderò attraverso le parti salienti.

What's Functional Programming All About? - lihaoyi

Questo articolo offre una delle migliori spiegazioni intuitive sulla programmazione funzionale. L'autore confronta una ricetta scritta in stile imperativo (una serie di passaggi che modificano un "miscuglio") con una scritta in stile funzionale (un diagramma di flusso di dati).

Leggi la sezione "Step by Step Imperative Recipes" per capire come la ricetta imperativa si basi sulla modifica sequenziale di uno stato (la ciotola mixture). Successivamente, leggi "'Functional Programming' Recipes" per vedere come la stessa ricetta viene rappresentata come un flusso di dati, dove ogni passaggio prende degli input e produce un nuovo output, senza modificare nulla. Infine, leggi la sezione "The Core of Functional Programming" che riassume magnificamente il concetto di data-flow.

Riflettiamo sull'analogia del Tiramisù:

  • Ricetta Imperativa: È una lista di comandi. "Sbatti le uova", "aggiungi lo zucchero", "mescola". C'è una variabile implicita, mixture, che viene continuamente modificata (mutata). Questo è un side effect. Se vuoi parallelizzare il lavoro o capire le dipendenze, devi analizzare attentamente ogni passo per capire cosa dipende da cosa.
  • Ricetta Funzionale (il diagramma): Mostra le dipendenze in modo esplicito. beat(eggs) produce beat_eggs. whip(cream) produce whipped_cream. Per ottenere folded_mixture, hai bisogno di cheese_mixture e whipped_cream. Il flusso dei dati è chiaro. Le funzioni sono pure: non modificano i loro ingredienti, ma ne creano di nuovi.

Questo è il vantaggio principale: le funzioni pure rendono il flusso dei dati esplicito, eliminando gli effetti collaterali nascosti. Il codice diventa più simile a un diagramma di flusso matematico, più facile da analizzare, ottimizzare e parallelizzare.


4. Esercizio Pratico: Identifica le Funzioni

Ora, mettiamo alla prova la tua comprensione. Per ciascuna delle seguenti funzioni Python, stabilisci se è pura o impura e spiega brevemente il perché.

Funzione 1:

import random

def get_random_choice(items):
    return random.choice(items)

Funzione 2:

def create_greeting(name):
    return f"Hello, {name}!"

Funzione 3:

user_cache = {}

def get_user_from_db(user_id):
    if user_id in user_cache:
        return user_cache[user_id]
    



    # Immagina che qui ci sia del codice che legge da un database
    user_data = f"data_for_{user_id}" 
    user_cache[user_id] = user_data
    return user_data

Funzione 4:

def add_item_to_list(items, item):
    items.append(item)
    return items
Clicca qui per vedere le soluzioni e le spiegazioni.

Funzione 1: Impura

  • Spiegazione: Non è deterministica. Chiamandola più volte con la stessa lista items, produrrà un output diverso a seconda del risultato di random.choice().

Funzione 2: Pura

  • Spiegazione: È deterministica (lo stesso name produce sempre lo stesso saluto) e non ha effetti collaterali. Il suo output dipende solo dal suo input.

Funzione 3: Impura

  • Spiegazione: Ha molteplici effetti collaterali e non è deterministica.
    1. Side Effect (Lettura): Legge da uno stato esterno (il database).
    2. Side Effect (Scrittura): Modifica uno stato esterno (la variabile globale user_cache).
    3. Non Determinismo: Il suo comportamento (e potenzialmente le sue performance) cambia dopo la prima esecuzione per un dato user_id, a causa della cache.

Funzione 4: Impura

  • Spiegazione: Questa è un po' più sottile. La funzione ha un effetto collaterale: modifica (muta) l'oggetto list che le viene passato come argomento. La lista items esiste al di fuori della funzione, e la funzione la sta alterando. Chi ha chiamato la funzione si ritroverà con la lista originale modificata, un effetto collaterale potenzialmente inaspettato.

Riepilogo e Prossimi Passi

In questa lezione abbiamo introdotto il concetto fondamentale di funzione pura, il mattone della programmazione funzionale.

Punti chiave:

  • Una funzione pura è deterministica (stesso input -> stesso output) e non ha effetti collaterali (non interagisce con il mondo esterno né lo modifica).
  • Gli effetti collaterali (side effects) rendono il codice più difficile da analizzare, testare e parallelizzare.
  • Il passaggio al pensiero funzionale è un passaggio dalla focalizzazione sul control-flow (sequenza di comandi) alla focalizzazione sul data-flow (trasformazione dei dati).

L'ultimo esempio dell'esercizio (add_item_to_list) ha evidenziato un tipo comune di effetto collaterale: la modifica di una struttura dati passata come argomento. Un modo per evitare questo problema è rendere le nostre strutture dati non modificabili.

Nella prossima lezione, esploreremo esattamente questo: i benefici dell'immutabilità. Vedremo come l'uso di dati immutabili in Python sia una tecnica chiave per scrivere codice in stile funzionale, più sicuro e prevedibile.