Parte IV — Potenza
Capitolo 15 — Le sequenze pigre
Finora, ogni lista che abbiamo costruito esisteva tutta in una volta, in memoria. Ma a volte vorresti ragionare su una sequenza enorme, o addirittura infinita — tutti i numeri naturali, per dire — senza calcolarla per intero. Le sequenze pigre rendono possibile proprio questo: producono i loro elementi uno alla volta, solo quando servono. «Pigre» non è un difetto: è la loro virtù.
Una sequenza senza fine
numeri_infiniti(1) è la sequenza 1, 2, 3, 4, ... che non finisce mai. Non puoi stamparla tutta, ovviamente; ma puoi chiederne i primi elementi con .prendi(n), e trasformarla in una lista vera con .a_lista():
variabile primi5 = numeri_infiniti(1).prendi(5).a_lista()
scrivi(primi5)[1, 2, 3, 4, 5]La chiave è che, fino al .prendi(5), non è stato calcolato niente: la sequenza infinita è solo una ricetta per produrre numeri. Solo quando chiedi i primi cinque, Leo ne genera cinque e si ferma. Questo è il senso della pigrizia: il lavoro si fa all'ultimo momento utile, e solo quel tanto che serve.
💡 L'idea — Come un rubinetto
Una sequenza pigra è come un rubinetto: l'acqua esce solo quando apri, e nella quantità che ti serve, non tutta insieme ad allagare la stanza. I valori si calcolano uno alla volta, nel momento in cui li chiedi. È per questo che puoi perfino avere sequenze infinite e prenderne tranquillamente solo i primi pezzi.
Trasformare senza materializzare
Il bello arriva quando concateni trasformazioni. .mappa(...) e .filtra(...) funzionano come le loro versioni sulle liste (Capitolo 9), ma restano pigre: non producono una lista intermedia, descrivono soltanto come trasformare ogni elemento quando verrà il suo turno.
variabile quadrati = numeri_infiniti(1).mappa(funzione (x) restituisci x * x fine).prendi(5).a_lista()
scrivi(quadrati)
variabile pari = numeri_infiniti(1).filtra(funzione (x) restituisci x % 2 == 0 fine).prendi(4).a_lista()
scrivi(pari)[1, 4, 9, 16, 25]
[2, 4, 6, 8]Rifletti su cosa succede nella seconda riga: stiamo filtrando i pari di una sequenza infinita e ne prendiamo quattro. Con le liste normali sarebbe impossibile — non puoi filtrare una lista infinita, non finiresti mai. Con le sequenze pigre funziona perché Leo genera numeri solo finché non ne ha trovati quattro pari, poi si ferma.
🦁 Curiosità — Più grande della memoria
Proprio perché ogni elemento si calcola solo nell'istante in cui serve, una sequenza pigra ti permette di lavorare con elenchi più grandi della memoria stessa del computer. Nessuno chiede mai di tenere tutto in mano nello stesso momento: si legge un valore, lo si usa e lo si lascia andare. È la differenza tra leggere un libro pagina per pagina e pretendere di tenerne a mente tutte le pagine in una volta sola.
Fermarsi al momento giusto
Oltre a .prendi(n), ci sono modi più espressivi per decidere dove fermarsi o da dove partire:
variabile piccoli = numeri_infiniti(1).finche(funzione (x) restituisci x < 6 fine).a_lista()
scrivi(piccoli)
variabile saltati = numeri_infiniti(1).salta(2).prendi(3).a_lista()
scrivi(saltati)[1, 2, 3, 4, 5]
[3, 4, 5].finche(funzione) prende gli elementi dall'inizio finché la funzione dà vero, poi si ferma: qui, tutti i numeri finché sono minori di 6. .salta(n) scarta i primi n elementi e continua da lì. Combinando salta, prendi, mappa, filtra e finche descrivi pipeline di trasformazione molto espressive, e nessuna materializza dati inutili.
Rendere pigra una sequenza qualunque
Non serve partire da numeri_infiniti: puoi rendere pigra una lista che hai già, con pigra(...). Serve quando vuoi applicare una catena di trasformazioni senza creare liste intermedie a ogni passo:
variabile s = pigra([1, 2, 3, 4, 5, 6])
variabile r = s.filtra(funzione (x) restituisci x % 2 == 0 fine).mappa(funzione (x) restituisci x * 10 fine).a_lista()
scrivi(r)
per ogni n in numeri_infiniti(10, 5).prendi(4) fai
scrivi(n)
fine[20, 40, 60]
10
15
20
25Nella prima parte, da [1..6] teniamo i pari e li moltiplichiamo per dieci, tutto in un flusso solo. Nella seconda, numeri_infiniti(10, 5) parte da 10 con passo 5 (10, 15, 20, 25, ...) e un per ogni ne consuma i primi quattro: le sequenze pigre si possono scorrere direttamente con per ogni, senza passare da .a_lista().
Quando usarle
Le sequenze pigre non sostituiscono le liste: le liste restano la scelta giusta per i dati di dimensione nota su cui vuoi tornare più volte. Le sequenze pigre brillano in tre situazioni: quando la sequenza è concettualmente infinita (flussi, generatori); quando è enorme e vuoi solo i primi risultati senza calcolare tutto; e quando concateni molte trasformazioni e vuoi evitare le liste intermedie. In tutti questi casi, la pigrizia non è un compromesso ma esattamente ciò che serve.
Prova tu
- Costruisci la sequenza dei multipli di 3 (
numeri_infiniticon il passo giusto, oppure.filtra) e stampane i primi dieci. - Usando
.finche, prendi tutti i quadrati dei numeri naturali finché restano sotto 200, e stampali. - Parti da una lista di parole, rendila pigra, tieni solo quelle lunghe più di quattro lettere e trasformale in maiuscolo; stampa il risultato con
.a_lista().