Haskell · concetti · valutazione
La valutazione lazy in Haskell, spiegata
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
- 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
filteremapsu 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 && undefinedrestituisceFalsesenza mai toccareundefined, 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— forzaain forma normale debole di testa prima di restituireb.($!)— applicazione strict:f $! xforzaxprima di applicaref.BangPatterns— l’estensione{-# LANGUAGE BangPatterns #-}ti lascia scriverelet !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.