Haskell · conceptos · evaluación
La evaluación perezosa en Haskell, explicada
La mayoría de los lenguajes son estrictos: una expresión se evalúa en cuanto se vincula a un nombre. Haskell es diferente — es perezoso (más precisamente, no estricto) por defecto: una expresión no se evalúa hasta que su resultado se necesita de verdad. Esta única decisión de diseño está detrás de algunos de los poderes más distintivos de Haskell y de su trampa más conocida. Esta guía explica qué es la evaluación perezosa, cómo funcionan los thunks, qué te aporta y cómo desactivarla cuando lo necesitas.
La idea en una frase
Bajo la evaluación perezosa, vincular un valor no lo calcula — Haskell almacena un cómputo diferido (un thunk) y solo lo fuerza cuando otro cómputo exige el resultado. Si el resultado nunca se exige, el trabajo nunca se hace.
Thunks: cómputo diferido
Cuando escribes let x = expensive 42, Haskell no ejecuta expensive. Crea un thunk — una promesa sin evaluar de expensive 42. El thunk se fuerza solo cuando algo necesita el valor real de x (por ejemplo, imprimirlo o hacer pattern matching sobre él). Fuérzalo una vez y el resultado se cachea para que no se vuelva a calcular.
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 Qué te aporta la pereza
- Estructuras de datos infinitas.
[1..]es la lista infinita de enteros.take 5 [1..]devuelve[1,2,3,4,5]porque solo se exigen los cinco primeros. - Composición sin desperdicio. Puedes encadenar
filterymapsobre una lista enorme y Haskell fusiona el trabajo, produciendo solo lo que el consumidor extrae — sin listas intermedias completas si nunca se exigen del todo. - Cortocircuito gratis.
False && undefineddevuelveFalsesin tocar nuncaundefined, porque el segundo argumento nunca se fuerza. - Autorreferencia. Clásicos de una línea como
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)definen el flujo infinito de Fibonacci — solo posible porque la estructura se construye de forma perezosa.
La trampa: las fugas de espacio
La pereza tiene un coste. Los thunks sin evaluar se acumulan en memoria, y una larga cadena de cómputos diferidos puede disparar silenciosamente el uso del heap — una fuga de espacio. El caso de manual es un fold por la izquierda perezoso que construye una torre gigantesca de thunks (((0 + 1) + 2) + 3) ... en lugar de ir sumando sobre la marcha:
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 solución es forzar la evaluación donde importa. Recurre al fold por la izquierda estricto foldl' (de Data.List) para las acumulaciones.
Forzar la evaluación estricta: seq, $! y BangPatterns
Haskell te da herramientas precisas para exigir la evaluación:
seq a b— fuerzaaa forma normal de cabeza débil antes de devolverb.($!)— aplicación estricta:f $! xfuerzaxantes de aplicarf.BangPatterns— la extensión{-# LANGUAGE BangPatterns #-}te permite escribirlet !x = ...para forzar una vinculación.
Una sutileza que conviene saber: forzar normalmente llega a la forma normal de cabeza débil (el constructor más externo), no a la evaluación completa. Para forzar en profundidad, usa force / deepseq del paquete deepseq.
Preguntas frecuentes
¿Es Haskell perezoso o no estricto? Técnicamente no estricto (una garantía semántica); GHC lo implementa mediante evaluación perezosa con thunks y compartición. En la práctica se dice "perezoso".
¿Por qué mi programa Haskell usa tanta memoria? A menudo es una fuga de espacio por thunks acumulados — comúnmente un foldl perezoso. Cambia a foldl' o añade rigor con seq/BangPatterns.
¿Qué es un thunk? Un cómputo diferido y sin evaluar almacenado en lugar de un valor, forzado solo cuando se exige el valor y luego cacheado.
¿La pereza hace que Haskell sea lento? No de forma inherente — puede evitar trabajo innecesario y permitir la fusión. Pero la pereza descuidada provoca fugas de espacio; saber cuándo forzar es parte de escribir buen Haskell.
La pereza es la misma maquinaria que hace que las demás abstracciones de Haskell se compongan limpiamente — consulta cómo funciona la secuenciación en nuestra guía sobre las mónadas en Haskell, y configura el toolchain para probar estos ejemplos en GHCi con la guía del compilador GHC.