coldwa.st
Tutte le guideProgrammazioneWebDatiStrumentiDatabaseHaskellConcettiCabal e buildToolchainCompilatorePrestazioniEditor e HLS

Haskell · concetti · valutazione

La valutazione lazy in Haskell, spiegata

Di ColdwastAggiornato il 14 giugno 20268 min di lettura#haskell#laziness#concepts
Codice sorgente colorato sullo schermo
Codice sorgente colorato sullo schermo — la valutazione lazy decide quando codice come questo calcola realmente un valore.

La maggior parte dei linguaggi è strict: un’espressione viene valutata non appena è legata a un nome. Haskell è diverso — per impostazione predefinita è lazy (più precisamente non strict): un’espressione non viene valutata finché il suo risultato non è veramente necessario. Questa singola scelta di progettazione sta dietro alcuni dei poteri più distintivi di Haskell e alla sua trappola più nota. Questa guida spiega cos’è la valutazione lazy, come funzionano i thunk, cosa ti dà e come disattivarla quando serve.

L’idea in una frase

Sotto la valutazione lazy, legare un valore non lo calcola — Haskell memorizza un calcolo differito (un thunk) e lo forza solo quando un altro calcolo richiede il risultato. Se il risultato non viene mai richiesto, il lavoro non viene mai svolto.

Thunk: il calcolo differito

Quando scrivi let x = expensive 42, Haskell non esegue expensive. Crea un thunk — una promessa non valutata di expensive 42. Il thunk viene forzato solo quando qualcosa ha bisogno del valore reale di x (per esempio stamparlo o farci pattern matching). Forzalo una volta, e il risultato viene messo in cache per non essere ricalcolato.

let x = expensive 42   -- nothing runs yet; x is a thunk
let y = x + 1          -- still nothing; y is a thunk too
print y                -- NOW x and y are forced and evaluated

Cosa ti dà la lazyness

Righe di codice su uno schermo scuro
Righe di codice sullo schermo — la lazyness ti lascia definire strutture infinite e valutare sempre solo la parte che consumi.
  • Strutture dati infinite. [1..] è la lista infinita degli interi. take 5 [1..] restituisce [1,2,3,4,5] perché solo i primi cinque vengono mai richiesti.
  • Composizione senza sprechi. Puoi concatenare filter e map su una lista enorme e Haskell fonde il lavoro, producendo solo ciò che il consumatore tira — niente liste intermedie complete se non vengono mai richieste per intero.
  • Cortocircuito gratuito. False && undefined restituisce False senza mai toccare undefined, perché il secondo argomento non viene mai forzato.
  • Auto-riferimento. One-liner classici come fibs = 0 : 1 : zipWith (+) fibs (tail fibs) definiscono il flusso infinito di Fibonacci — possibile solo perché la struttura è costruita in modo lazy.

La trappola: i memory leak

La lazyness ha un costo. I thunk non valutati si accumulano in memoria, e una lunga catena di calcoli differiti può gonfiare silenziosamente l’uso dello heap — un memory leak (space leak). Il caso da manuale è una fold a sinistra lazy che costruisce una torre gigantesca di thunk (((0 + 1) + 2) + 3) ... invece di sommare man mano:

foldl (+) 0 [1..1000000]    -- builds a million thunks, may blow the stack
foldl' (+) 0 [1..1000000]   -- strict fold: adds as it goes, constant space

La soluzione è forzare la valutazione dove conta. Ricorri alla fold a sinistra strict foldl' (da Data.List) per le accumulazioni.

Forzare la strictness: seq, $! e BangPatterns

Haskell ti dà strumenti precisi per richiedere la valutazione:

  • seq a b — forza a in forma normale debole di testa prima di restituire b.
  • ($!) — applicazione strict: f $! x forza x prima di applicare f.
  • BangPatterns — l’estensione {-# LANGUAGE BangPatterns #-} ti lascia scrivere let !x = ... per forzare un binding.

Una sottigliezza da conoscere: forzare raggiunge tipicamente la forma normale debole di testa (il costruttore più esterno), non la valutazione completa. Per forzare in profondità, usa force / deepseq del pacchetto deepseq.

FAQ

Haskell è lazy o non strict? Tecnicamente non strict (una garanzia semantica); GHC lo implementa tramite la valutazione lazy con thunk e condivisione. In pratica si dice « lazy ».

Perché il mio programma Haskell usa così tanta memoria? Spesso un memory leak dovuto a thunk accumulati — comunemente un foldl lazy. Passa a foldl' o aggiungi strictness con seq/BangPatterns.

Cos’è un thunk? Un calcolo non valutato e differito memorizzato al posto di un valore, forzato solo quando il valore è richiesto, e poi messo in cache.

La lazyness rende Haskell lento? Non intrinsecamente — può evitare lavoro inutile e abilitare la fusione. Ma una lazyness trascurata causa memory leak; sapere quando forzare fa parte dello scrivere buon Haskell.

La lazyness è lo stesso meccanismo che fa comporre in modo pulito le altre astrazioni di Haskell — guarda come funziona la sequenzializzazione nella nostra guida sulle monadi in Haskell, e configura la toolchain per provare questi esempi in GHCi con la guida al compilatore GHC.

Guida indipendente, mantenuta dalla community. coldwa.st è un sito di risorse di programmazione; questo articolo è uno scritto esplicativo nuovo e originale sulla valutazione lazy in Haskell, senza affiliazione con alcun progetto né redatto da esso. Il codice riflette il comportamento standard di Haskell/GHC — verifica con la documentazione GHC attuale.